在C++标准模板库(STL)中,list是一个双向链表容器,它允许在常数时间内进行任意位置的插入和删除操作。与vector这种连续存储的容器不同,list采用非连续的存储方式,通过指针将各个元素链接在一起。
list的核心特点包括:
cpp复制#include <iostream>
#include <list>
int main() {
// 创建一个空list
std::list<int> list1;
// 创建包含5个元素的list,初始值为0
std::list<int> list2(5);
// 创建并初始化list
std::list<int> list3 = {1, 2, 3, 4, 5};
// 通过迭代器范围初始化
int arr[] = {6, 7, 8, 9, 10};
std::list<int> list4(arr, arr + sizeof(arr)/sizeof(arr[0]));
return 0;
}
list提供了丰富的成员函数来操作容器:
cpp复制std::list<int> myList = {1, 2, 3};
// 在末尾添加元素
myList.push_back(4); // 1,2,3,4
// 在开头添加元素
myList.push_front(0); // 0,1,2,3,4
// 删除末尾元素
myList.pop_back(); // 0,1,2,3
// 删除开头元素
myList.pop_front(); // 1,2,3
// 获取大小
size_t size = myList.size(); // 3
// 判断是否为空
bool isEmpty = myList.empty(); // false
// 清空list
myList.clear(); // 清空所有元素
由于list不支持随机访问,必须使用迭代器来遍历元素:
cpp复制std::list<int> myList = {1, 2, 3, 4, 5};
// 正向遍历
for(std::list<int>::iterator it = myList.begin(); it != myList.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 反向遍历
for(std::list<int>::reverse_iterator rit = myList.rbegin(); rit != myList.rend(); ++rit) {
std::cout << *rit << " ";
}
std::cout << std::endl;
// C++11范围for循环
for(int num : myList) {
std::cout << num << " ";
}
std::cout << std::endl;
list在任何位置的插入和删除操作都非常高效:
cpp复制std::list<int> myList = {1, 2, 3, 4, 5};
// 在指定位置插入元素
auto it = myList.begin();
std::advance(it, 2); // 移动到第三个元素
myList.insert(it, 10); // 1,2,10,3,4,5
// 删除指定位置的元素
it = myList.begin();
std::advance(it, 3);
myList.erase(it); // 1,2,10,4,5
// 删除特定值的所有元素
myList.remove(10); // 1,2,4,5
// 删除满足条件的元素
myList.remove_if([](int n){ return n % 2 == 0; }); // 1,5
list提供了专门的排序算法,比通用算法更高效:
cpp复制std::list<int> list1 = {5, 3, 1, 4, 2};
std::list<int> list2 = {10, 8, 6, 9, 7};
// 升序排序
list1.sort(); // 1,2,3,4,5
list2.sort(); // 6,7,8,9,10
// 合并两个已排序的list
list1.merge(list2); // list1: 1,2,3,4,5,6,7,8,9,10
// list2变为空
// 降序排序
list1.sort(std::greater<int>()); // 10,9,8,7,6,5,4,3,2,1
cpp复制std::list<int> myList = {1, 2, 2, 3, 3, 3, 4, 4, 4, 4};
// 去重(必须先排序)
myList.sort();
myList.unique(); // 1,2,3,4
// 反转list
myList.reverse(); // 4,3,2,1
首先需要定义链表节点的结构:
cpp复制template<typename T>
struct ListNode {
T data; // 数据域
ListNode<T>* prev; // 前驱指针
ListNode<T>* next; // 后继指针
// 构造函数
ListNode(const T& val = T(),
ListNode<T>* p = nullptr,
ListNode<T>* n = nullptr)
: data(val), prev(p), next(n) {}
};
实现list类的基本框架:
cpp复制template<typename T>
class List {
private:
ListNode<T>* head; // 头节点
ListNode<T>* tail; // 尾节点
size_t size; // 元素个数
public:
// 构造函数
List() : size(0) {
head = new ListNode<T>();
tail = new ListNode<T>();
head->next = tail;
tail->prev = head;
}
// 析构函数
~List() {
clear();
delete head;
delete tail;
}
// 其他成员函数...
};
实现list迭代器是模拟实现的关键部分:
cpp复制template<typename T>
class ListIterator {
private:
ListNode<T>* current; // 当前节点指针
public:
// 构造函数
ListIterator(ListNode<T>* p = nullptr) : current(p) {}
// 解引用操作符
T& operator*() const {
return current->data;
}
// 箭头操作符
T* operator->() const {
return &(current->data);
}
// 前置++
ListIterator& operator++() {
current = current->next;
return *this;
}
// 后置++
ListIterator operator++(int) {
ListIterator tmp = *this;
++(*this);
return tmp;
}
// 前置--
ListIterator& operator--() {
current = current->prev;
return *this;
}
// 后置--
ListIterator operator--(int) {
ListIterator tmp = *this;
--(*this);
return tmp;
}
// 比较操作符
bool operator==(const ListIterator& rhs) const {
return current == rhs.current;
}
bool operator!=(const ListIterator& rhs) const {
return current != rhs.current;
}
};
实现push_back、push_front等基本操作:
cpp复制template<typename T>
class List {
// ... 其他代码
public:
// 在末尾插入元素
void push_back(const T& value) {
insert(end(), value);
}
// 在开头插入元素
void push_front(const T& value) {
insert(begin(), value);
}
// 删除末尾元素
void pop_back() {
erase(--end());
}
// 删除开头元素
void pop_front() {
erase(begin());
}
// 在指定位置插入元素
iterator insert(iterator pos, const T& value) {
ListNode<T>* newNode = new ListNode<T>(value, pos.current->prev, pos.current);
pos.current->prev->next = newNode;
pos.current->prev = newNode;
++size;
return iterator(newNode);
}
// 删除指定位置的元素
iterator erase(iterator pos) {
ListNode<T>* toDelete = pos.current;
iterator retVal(toDelete->next);
toDelete->prev->next = toDelete->next;
toDelete->next->prev = toDelete->prev;
delete toDelete;
--size;
return retVal;
}
// 清空list
void clear() {
while(!empty()) {
pop_front();
}
}
// 获取大小
size_t getSize() const {
return size;
}
// 判断是否为空
bool empty() const {
return size == 0;
}
// 获取迭代器
iterator begin() {
return iterator(head->next);
}
iterator end() {
return iterator(tail);
}
};
将上述部分组合起来,形成一个简单的list实现:
cpp复制#include <iostream>
template<typename T>
struct ListNode {
T data;
ListNode<T>* prev;
ListNode<T>* next;
ListNode(const T& val = T(),
ListNode<T>* p = nullptr,
ListNode<T>* n = nullptr)
: data(val), prev(p), next(n) {}
};
template<typename T>
class ListIterator {
// ... 迭代器实现代码
};
template<typename T>
class List {
private:
ListNode<T>* head;
ListNode<T>* tail;
size_t size;
public:
typedef ListIterator<T> iterator;
List() : size(0) {
head = new ListNode<T>();
tail = new ListNode<T>();
head->next = tail;
tail->prev = head;
}
~List() {
clear();
delete head;
delete tail;
}
// ... 其他成员函数实现
void print() const {
ListNode<T>* current = head->next;
while(current != tail) {
std::cout << current->data << " ";
current = current->next;
}
std::cout << std::endl;
}
};
int main() {
List<int> myList;
myList.push_back(1);
myList.push_back(2);
myList.push_back(3);
myList.print(); // 输出: 1 2 3
myList.push_front(0);
myList.print(); // 输出: 0 1 2 3
myList.pop_back();
myList.print(); // 输出: 0 1 2
myList.pop_front();
myList.print(); // 输出: 1 2
return 0;
}
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 插入/删除 | O(1) | 任意位置操作都很高效 |
| 访问元素 | O(n) | 需要遍历到指定位置 |
| 排序 | O(n log n) | 使用成员函数sort更高效 |
| 合并 | O(n) | 合并两个已排序的list |
| 查找 | O(n) | 需要遍历整个list |
list特别适合以下场景:
list不适用于:
list的迭代器在以下情况下会失效:
cpp复制std::list<int> myList = {1, 2, 3, 4, 5};
auto it = myList.begin();
++it; // 指向2
myList.erase(it); // it现在失效
// 不能再使用it
list的每个元素需要额外存储两个指针,内存开销较大:
对于小型元素,内存开销可能比数据本身还大。
list常被用于实现LRU(最近最少使用)缓存算法:
cpp复制#include <list>
#include <unordered_map>
template<typename K, typename V>
class LRUCache {
private:
size_t capacity;
std::list<std::pair<K, V>> cache;
std::unordered_map<K, typename std::list<std::pair<K, V>>::iterator> map;
public:
LRUCache(size_t cap) : capacity(cap) {}
V get(K key) {
auto it = map.find(key);
if(it == map.end()) {
return V(); // 返回默认值
}
// 移动到链表头部
cache.splice(cache.begin(), cache, it->second);
return it->second->second;
}
void put(K key, V value) {
auto it = map.find(key);
if(it != map.end()) {
// 已存在,更新值并移动到头部
it->second->second = value;
cache.splice(cache.begin(), cache, it->second);
return;
}
// 检查容量
if(cache.size() == capacity) {
// 删除最久未使用的元素
auto last = cache.back();
map.erase(last.first);
cache.pop_back();
}
// 插入新元素到头部
cache.emplace_front(key, value);
map[key] = cache.begin();
}
};
list适合实现简单的消息队列:
cpp复制#include <list>
#include <string>
#include <iostream>
class MessageQueue {
private:
std::list<std::string> queue;
public:
void enqueue(const std::string& msg) {
queue.push_back(msg);
}
std::string dequeue() {
if(queue.empty()) {
return "";
}
std::string msg = queue.front();
queue.pop_front();
return msg;
}
void processAll() {
while(!queue.empty()) {
std::string msg = dequeue();
std::cout << "Processing: " << msg << std::endl;
// 实际处理逻辑...
}
}
};
int main() {
MessageQueue mq;
mq.enqueue("Message 1");
mq.enqueue("Message 2");
mq.enqueue("Message 3");
mq.processAll();
return 0;
}
| 特性 | list | vector |
|---|---|---|
| 存储方式 | 非连续 | 连续 |
| 随机访问 | 不支持(O(n)) | 支持(O(1)) |
| 插入/删除 | 任意位置O(1) | 末尾O(1),其他位置O(n) |
| 内存使用 | 每个元素额外两个指针 | 只需存储元素 |
| 迭代器失效 | 只有被删除的元素失效 | 插入/删除可能导致全部失效 |
| 缓存友好性 | 差 | 好 |
| 特性 | list | deque |
|---|---|---|
| 存储方式 | 非连续 | 分块连续 |
| 随机访问 | 不支持(O(n)) | 支持(O(1)) |
| 插入/删除 | 任意位置O(1) | 头尾O(1),中间O(n) |
| 内存使用 | 每个元素额外两个指针 | 分块管理,额外开销较小 |
| 迭代器失效 | 只有被删除的元素失效 | 插入可能导致全部失效 |
C++11引入了emplace系列函数,可以直接在容器中构造元素,避免临时对象:
cpp复制std::list<std::pair<int, std::string>> myList;
// C++98方式
myList.push_back(std::pair<int, std::string>(1, "one"));
// C++11方式
myList.emplace_back(1, "one"); // 直接在容器中构造pair
C++11支持使用初始化列表构造list:
cpp复制std::list<int> myList = {1, 2, 3, 4, 5}; // 直接初始化
简化遍历操作:
cpp复制for(const auto& num : myList) {
std::cout << num << " ";
}
splice操作现在返回迭代器:
cpp复制std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6};
// 将list2的第一个元素移动到list1末尾
auto it = list1.splice(list1.end(), list2, list2.begin());
// it指向被移动的元素(4)
在以下情况选择list:
否则考虑vector或deque。
如果知道大概的元素数量,可以预先分配节点:
cpp复制std::list<MyClass> bigList;
// 预先分配1000个节点
for(int i = 0; i < 1000; ++i) {
bigList.emplace_back();
}
list特有的算法通常比通用算法更高效:
cpp复制std::list<int> myList = {5, 3, 1, 4, 2};
// 正确方式 - 使用成员函数
myList.sort();
// 错误方式 - 不能使用std::sort
// std::sort(myList.begin(), myList.end()); // 编译错误
频繁插入删除可能导致内存碎片,可以考虑:
list允许使用自定义分配器来管理内存:
cpp复制#include <list>
#include <memory>
template<typename T>
class SimpleAllocator {
public:
using value_type = T;
SimpleAllocator() = default;
template<typename U>
SimpleAllocator(const SimpleAllocator<U>&) {}
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t) {
::operator delete(p);
}
};
int main() {
std::list<int, SimpleAllocator<int>> customList;
customList.push_back(1);
customList.push_back(2);
customList.push_back(3);
for(int num : customList) {
std::cout << num << " ";
}
return 0;
}
list的特殊数据结构使得标准算法std::sort无法高效工作,因为std::sort需要随机访问迭代器。list::sort使用归并排序算法,专门为链表结构优化。
在C++98中,list的size()可以是O(n),因为标准允许实现选择缓存size还是遍历计算。C++11规定size()必须是O(1),所有现代实现都遵守这一点。
对于频繁查找的场景:
STL容器本身不是线程安全的。如果需要在多线程环境下使用list,需要:
比较list和vector在中间插入的性能:
cpp复制#include <list>
#include <vector>
#include <chrono>
#include <iostream>
const int ELEMENTS = 100000;
void testListInsert() {
std::list<int> myList;
auto start = std::chrono::high_resolution_clock::now();
for(int i = 0; i < ELEMENTS; ++i) {
myList.insert(std::next(myList.begin(), myList.size()/2), i);
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "List insert time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;
}
void testVectorInsert() {
std::vector<int> myVector;
auto start = std::chrono::high_resolution_clock::now();
for(int i = 0; i < ELEMENTS; ++i) {
myVector.insert(myVector.begin() + myVector.size()/2, i);
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Vector insert time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;
}
int main() {
testListInsert();
testVectorInsert();
return 0;
}
典型结果:
code复制List insert time: 120 ms
Vector insert time: 4500 ms
比较list和vector的遍历速度:
cpp复制void testListTraversal() {
std::list<int> myList(ELEMENTS, 1);
int sum = 0;
auto start = std::chrono::high_resolution_clock::now();
for(int num : myList) {
sum += num;
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "List traversal time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;
}
void testVectorTraversal() {
std::vector<int> myVector(ELEMENTS, 1);
int sum = 0;
auto start = std::chrono::high_resolution_clock::now();
for(int num : myVector) {
sum += num;
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Vector traversal time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms" << std::endl;
}
典型结果:
code复制List traversal time: 2 ms
Vector traversal time: 0.5 ms
创建一个在元素变化时通知观察者的list:
cpp复制#include <list>
#include <functional>
#include <vector>
template<typename T>
class ObservableList {
private:
std::list<T> data;
std::vector<std::function<void(const T&)>> observers;
public:
void addObserver(std::function<void(const T&)> observer) {
observers.push_back(observer);
}
void push_back(const T& value) {
data.push_back(value);
notify(value);
}
void pop_back() {
if(!data.empty()) {
T value = data.back();
data.pop_back();
notify(value);
}
}
private:
void notify(const T& value) {
for(auto& observer : observers) {
observer(value);
}
}
};
int main() {
ObservableList<int> myList;
// 添加观察者
myList.addObserver([](const int& value) {
std::cout << "Element changed: " << value << std::endl;
});
myList.push_back(1);
myList.push_back(2);
myList.pop_back();
return 0;
}
实现一个简单的线程安全list:
cpp复制#include <list>
#include <mutex>
template<typename T>
class ThreadSafeList {
private:
std::list<T> data;
mutable std::mutex mtx;
public:
void push_back(const T& value) {
std::lock_guard<std::mutex> lock(mtx);
data.push_back(value);
}
bool pop_front(T& value) {
std::lock_guard<std::mutex> lock(mtx);
if(data.empty()) {
return false;
}
value = data.front();
data.pop_front();
return true;
}
size_t size() const {
std::lock_guard<std::mutex> lock(mtx);
return data.size();
}
bool empty() const {
std::lock_guard<std::mutex> lock(mtx);
return data.empty();
}
};
错误示例:
cpp复制std::list<int> myList = {1, 2, 3, 4, 5};
for(auto it = myList.begin(); it != myList.end(); ++it) {
if(*it == 3) {
myList.erase(it); // 错误!erase后it失效
}
}
正确做法:
cpp复制std::list<int> myList = {1, 2, 3, 4, 5};
for(auto it = myList.begin(); it != myList.end(); ) {
if(*it == 3) {
it = myList.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
错误示例:
cpp复制std::list<int> bigList;
// 低效的插入方式
for(int i = 0; i < 1000000; ++i) {
bigList.insert(std::next(bigList.begin(), bigList.size()/2), i);
}
改进方案:
cpp复制std::list<int> bigList;
// 先插入到末尾,然后整体移动
for(int i = 0; i < 1000000; ++i) {
bigList.push_back(i);
}
// 如果需要,可以使用splice进行批量移动
错误示例:
cpp复制std::list<int*> ptrList;
for(int i = 0; i < 10; ++i) {
ptrList.push_back(new int(i));
}
// 忘记释放内存
正确做法:
cpp复制std::list<int*> ptrList;
for(int i = 0; i < 10; ++i) {
ptrList.push_back(new int(i));
}
// 释放内存
while(!ptrList.empty()) {
delete ptrList.front();
ptrList.pop_front();
}
// 或者使用智能指针
std::list<std::unique_ptr<int>> safeList;
for(int i = 0; i < 10; ++i) {
safeList.push_back(std::make_unique<int>(i));
}
// 自动管理内存
C++20允许直接从范围构造list,无需指定开始和结束迭代器:
cpp复制std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> myList(vec); // C++20
新的std::erase和std::erase_if算法:
cpp复制std::list<int> myList = {1, 2, 3, 4, 5};
// 删除所有值为2的元素
std::erase(myList, 2);
// 删除所有偶数
std::erase_if(myList, [](int n){ return n % 2 == 0; });
C++20引入了三路比较运算符(<=>),list可以利用这一特性:
cpp复制#include <compare>
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {1, 2, 4};
auto cmp = list1 <=> list2;
if(cmp < 0) {
std::cout << "list1 < list2" << std::endl;
} else if(cmp > 0) {
std::cout << "list1 > list2" << std::endl;
} else {
std::cout << "list1 == list2" << std::endl;
}
不同平台下list的内存布局可能不同:
混合不同编译器版本编译的代码时需要注意:
预分配节点池减少内存分配开销:
cpp复制#include <memory>
template<typename T>
class ListNodePool {
private:
std::list<std::unique_ptr<ListNode<T>>> pool;
public:
ListNode<T>* allocate(const T& value, ListNode<T>* prev, ListNode<T>* next) {
if(pool.empty()) {
return new ListNode<T>(value, prev, next);
}
auto node = pool.back().release();
pool.pop_back();
node->data = value;
node->prev = prev;
node->next = next;
return node;
}
void deallocate(ListNode<T>* node) {
pool.emplace_back(node);
}
~ListNodePool() {
pool.clear();
}
};
template<typename T>
class PooledList {
// 使用ListNodePool管理节点...
};
利用splice进行批量操作:
cpp复制std::list<int> source = {1, 2, 3, 4, 5};
std::list<int> target;
// 批量移动元素
target.splice(target.end(), source,
std::next(source.begin()),
std::prev(source.end()));
// target: 2,3,4
// source: 1,5
实现缓存友好的小型链表:
cpp复制template<typename T, size_t ChunkSize = 64>
class ChunkedList {
private:
struct Chunk {
T data[ChunkSize];
Chunk* prev;
Chunk* next;
size_t size;
};
Chunk* head;
Chunk* tail;
// ... 其他实现
};
在大型项目中:
cpp复制// 使用类型别名简化代码
template<typename T>
using NodeList = std::list<std::pair<T, std::shared_ptr<SomeComplexType>>>;
// 在项目中一致使用
NodeList<int> myNodeList;
cpp复制template<typename T>
void printList(const std::list<T>& lst) {
for(const auto& elem : lst) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
完善的list相关测试应包括:
list是C++标准库中一个重要的顺序容器,它提供了高效的任意位置插入和删除操作。通过深入了解其实现原理和使用技巧,可以更好地在实际项目中发挥其优势。
对于想要进一步学习list和类似数据结构的开发者,建议:
在实际项目中,我经常发现list被过度使用或错误使用。理解其特性和适用场景,结合具体需求选择合适的容器,是写出高效C++代码的重要一环。对于性能敏感的场景,一定要进行基准测试,不要仅凭理论分析做决定。