1. list容器概述
在C++标准模板库(STL)中,list是一个双向链表容器,它提供了高效的插入和删除操作。与vector这样的顺序容器不同,list基于节点存储,每个元素都存储在自己的内存块中,并通过指针与其他元素相连。
list的核心特点:
- 双向链表结构:每个节点包含指向前驱和后继的指针
- 带头节点:包含一个不存储实际数据的哨兵节点,简化边界条件处理
- 循环链表:最后一个节点指向头节点,头节点指向最后一个节点
- 非连续存储:元素在内存中不是连续存储的
这种结构使得list在以下场景表现优异:
- 需要频繁在任意位置插入/删除元素
- 不需要随机访问元素
- 元素大小较大,移动成本高
2. list常用接口详解
2.1 构造函数与初始化
list提供了多种构造函数来满足不同初始化需求:
cpp复制// 默认构造:创建空list
list<int> lt1;
// 填充构造:创建包含n个val的list
list<int> lt2(5, 100); // 5个100
// 范围构造:用迭代器范围[first, last)初始化
int arr[] = {1,2,3,4,5};
list<int> lt3(arr, arr+5);
// 拷贝构造
list<int> lt4(lt3);
实际开发中,范围构造特别有用,可以方便地从其他容器或数组初始化list。需要注意的是,list的构造过程是深拷贝,每个元素都会被独立创建。
2.2 迭代器使用技巧
list的迭代器是双向迭代器,支持++和--操作,但不支持随机访问(如+/-运算):
cpp复制list<int> mylist = {1,2,3,4,5};
// 正向遍历
for(auto it = mylist.begin(); it != mylist.end(); ++it) {
cout << *it << " ";
}
// 反向遍历
for(auto rit = mylist.rbegin(); rit != mylist.rend(); ++rit) {
cout << *rit << " ";
}
重要注意事项:
- end()返回的是尾后迭代器,不是最后一个元素
- 反向迭代器的++操作实际上是向前移动
- 迭代器失效只发生在元素被删除时
2.3 容量查询与元素访问
list提供了一些基本的容量查询接口:
cpp复制list<int> mylist = {1,2,3};
cout << "Size: " << mylist.size() << endl; // 3
cout << "Empty: " << mylist.empty() << endl; // 0 (false)
// 访问首尾元素
cout << "Front: " << mylist.front() << endl; // 1
cout << "Back: " << mylist.back() << endl; // 3
需要注意的是,size()操作在C++11前可能是O(n)复杂度,之后标准要求为O(1)。如果需要在旧编译器上获得最佳性能,可以考虑使用empty()代替size() == 0的判断。
2.4 元素修改操作
list的核心优势在于高效的插入和删除操作:
cpp复制list<int> mylist = {1,2,3};
// 头尾操作
mylist.push_front(0); // 头部插入
mylist.pop_front(); // 头部删除
mylist.push_back(4); // 尾部插入
mylist.pop_back(); // 尾部删除
// 任意位置插入
auto it = mylist.begin();
advance(it, 1); // 移动到第二个位置
mylist.insert(it, 10); // 在第二个位置插入10
// 删除元素
it = mylist.begin();
advance(it, 2);
mylist.erase(it); // 删除第三个元素
关键点:
- 插入操作不会使任何迭代器失效
- 删除操作只会使指向被删除元素的迭代器失效
- 头尾操作都是O(1)时间复杂度
3. list迭代器失效问题深入
迭代器失效是STL容器使用中的常见陷阱。对于list来说,情况相对简单:
cpp复制list<int> mylist = {1,2,3,4,5};
auto it = mylist.begin();
++it; // 指向第二个元素
// 正确做法:获取erase返回的新迭代器
it = mylist.erase(it);
// 错误做法:使用已失效的迭代器
// mylist.erase(it);
// ++it; // 未定义行为!
唯一会使迭代器失效的操作是erase,它会使被删除元素的迭代器失效。insert操作不会导致任何迭代器失效,这是list相对于vector的一大优势。
4. list的模拟实现
理解list的最好方式就是自己实现一个简化版本。下面我们分析关键实现细节:
4.1 节点结构
cpp复制template<class T>
struct list_node {
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& data = T())
:_data(data)
,_next(nullptr)
,_prev(nullptr)
{}
};
每个节点包含数据域和两个指针,分别指向前驱和后继节点。默认构造函数使用T()进行值初始化。
4.2 迭代器设计
list迭代器是对节点指针的封装,需要实现指针-like的操作:
cpp复制template<class T, class Ref, class Ptr>
struct list_iterator {
typedef list_node<T> Node;
Node* _node;
// 解引用操作符
Ref operator*() { return _node->_data; }
// 成员访问操作符
Ptr operator->() { return &_node->_data; }
// 前置++
self& operator++() {
_node = _node->_next;
return *this;
}
// 后置++
self operator++(int) {
self tmp(*this);
_node = _node->_next;
return tmp;
}
// 比较操作符
bool operator!=(const self& it) const {
return _node != it._node;
}
};
通过模板参数Ref和Ptr,可以同时支持普通迭代器和const迭代器,避免了代码重复。
4.3 核心容器实现
list类的主要职责是管理节点和提供接口:
cpp复制template<class T>
class list {
typedef list_node<T> Node;
Node* _head; // 哨兵节点
size_t _size;
public:
// 构造函数
list() {
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
// 插入操作
iterator insert(iterator pos, const T& val) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* new_node = new Node(val);
new_node->_next = cur;
new_node->_prev = prev;
prev->_next = new_node;
cur->_prev = new_node;
++_size;
return new_node;
}
// 删除操作
iterator erase(iterator pos) {
Node* prev = pos._node->_prev;
Node* next = pos._node->_next;
prev->_next = next;
next->_prev = prev;
delete pos._node;
--_size;
return next;
}
// 其他接口...
};
实现时需要注意:
- 始终保持循环链表的完整性
- 正确维护_size计数
- 处理边界条件(如空链表)
5. list与vector的深度对比
5.1 性能特性对比
| 特性 | vector | list |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 头部插入/删除 | O(n) | O(1) |
| 尾部插入/删除 | O(1)(均摊) | O(1) |
| 中间插入/删除 | O(n) | O(1)(已知位置) |
| 内存布局 | 连续 | 非连续 |
| 空间开销 | 较小(仅需存储数据) | 较大(每个节点需要两个指针) |
| 缓存友好性 | 好 | 差 |
5.2 迭代器差异
vector的迭代器是原生指针,支持所有指针运算:
cpp复制vector<int>::iterator it = v.begin();
it += 5; // 随机访问
list的迭代器是类类型,只支持双向移动:
cpp复制list<int>::iterator it = l.begin();
++it; // 正确
it += 2; // 错误!
5.3 适用场景选择
选择vector当:
- 需要频繁随机访问元素
- 元素数量相对稳定,插入删除主要在尾部
- 元素较小,移动成本低
- 需要最大化缓存利用率
选择list当:
- 需要频繁在任意位置插入删除
- 元素较大,移动成本高
- 不需要随机访问
- 需要保证插入删除不影响其他元素的迭代器
6. 实际应用经验分享
6.1 性能优化技巧
- 批量插入优化:
cpp复制// 低效方式:多次调用insert
for(int i=0; i<1000; ++i) {
mylist.insert(pos, i);
}
// 高效方式:使用范围insert
vector<int> temp(1000);
iota(temp.begin(), temp.end(), 0);
mylist.insert(pos, temp.begin(), temp.end());
- 遍历优化:
cpp复制// 对于频繁访问,可以考虑转为vector处理
vector<int> temp(mylist.begin(), mylist.end());
// ...处理temp...
mylist.assign(temp.begin(), temp.end());
6.2 常见陷阱
- 失效迭代器:
cpp复制auto it1 = mylist.begin();
auto it2 = --mylist.end();
mylist.erase(it1);
// it1现在失效,但it2仍然有效
- 性能误用:
cpp复制// 糟糕的随机访问模式
for(size_t i=0; i<mylist.size(); ++i) {
// 通过advance模拟随机访问 - O(n)复杂度!
auto it = mylist.begin();
advance(it, i);
cout << *it;
}
6.3 与其他容器配合
list常与其他STL容器配合使用:
cpp复制// list与map配合实现LRU缓存
class LRUCache {
list<pair<int,int>> _list;
unordered_map<int, list<pair<int,int>>::iterator> _map;
size_t _capacity;
public:
int get(int key) {
auto it = _map.find(key);
if(it == _map.end()) return -1;
_list.splice(_list.begin(), _list, it->second);
return it->second->second;
}
void put(int key, int value) {
// ...实现略...
}
};
list的splice操作是它独有的高效特性,可以在O(1)时间内将节点从一个list转移到另一个list。