1. C++ STL list容器概述
在C++标准模板库(STL)中,list是一个双向链表容器,它允许在常数时间内进行任意位置的插入和删除操作。与vector和deque等顺序容器不同,list不支持随机访问,但它在中间位置插入和删除元素时具有显著优势。
list容器在 头文件中定义,其基本声明形式为:
std::list<T> myList;其中T是存储在列表中的元素类型。
2. list的核心特性与实现原理
2.1 双向链表结构
list的实现基于双向链表,每个节点包含:
- 数据部分:存储实际元素值
- 前驱指针:指向前一个节点
- 后继指针:指向后一个节点
这种结构使得list具有以下特点:
- 非连续内存存储
- 动态大小调整
- 高效的插入/删除操作
2.2 时间复杂度分析
| 操作 | 时间复杂度 |
|---|---|
| 插入/删除 | O(1) |
| 随机访问 | O(n) |
| 查找 | O(n) |
| 排序 | O(n log n) |
3. list的基本用法详解
3.1 创建和初始化list
// 空list std::list<int> list1; // 指定初始大小 std::list<int> list2(5); // 5个默认构造的元素 // 指定初始大小和值 std::list<int> list3(5, 100); // 5个值为100的元素 // 通过迭代器初始化 int arr[] = {1,2,3,4,5}; std::list<int> list4(arr, arr+5); // 拷贝构造 std::list<int> list5(list4); // 移动构造 std::list<int> list6(std::move(list5)); // 初始化列表(C++11) std::list<int> list7 = {1,2,3,4,5};3.2 元素访问操作
由于list不支持随机访问,只能通过迭代器或特定成员函数访问元素:
std::list<int> myList = {1,2,3,4,5}; // 访问首元素 std::cout << myList.front(); // 1 // 访问尾元素 std::cout << myList.back(); // 5 // 通过迭代器访问 for(auto it = myList.begin(); it != myList.end(); ++it) { std::cout << *it << " "; }注意:list没有提供operator[]或at()方法,因为它无法在常数时间内实现随机访问。
3.3 修改操作
插入元素
std::list<int> myList = {1,2,3}; // 在末尾插入 myList.push_back(4); // 1,2,3,4 // 在开头插入 myList.push_front(0); // 0,1,2,3,4 // 在指定位置插入 auto it = myList.begin(); std::advance(it, 2); // 移动到第3个位置 myList.insert(it, 10); // 0,1,10,2,3,4 // 插入多个相同元素 myList.insert(it, 3, 5); // 在it位置插入3个5 // 插入范围 std::vector<int> vec = {7,8,9}; myList.insert(it, vec.begin(), vec.end());删除元素
std::list<int> myList = {0,1,2,3,4,5}; // 删除末尾元素 myList.pop_back(); // 0,1,2,3,4 // 删除开头元素 myList.pop_front(); // 1,2,3,4 // 删除指定位置元素 auto it = myList.begin(); std::advance(it, 2); myList.erase(it); // 1,2,4 // 删除范围 myList.erase(myList.begin(), myList.end()); // 清空list // 删除特定值 myList.remove(2); // 删除所有值为2的元素3.4 容量操作
std::list<int> myList = {1,2,3}; // 检查是否为空 bool isEmpty = myList.empty(); // 获取元素数量 size_t size = myList.size(); // 调整大小 myList.resize(5); // 1,2,3,0,0 myList.resize(2); // 1,2 myList.resize(5, 9); // 1,2,9,9,94. list的高级操作
4.1 排序操作
list提供了专门的sort()成员函数,比通用算法std::sort()更高效:
std::list<int> myList = {3,1,4,2,5}; // 升序排序 myList.sort(); // 1,2,3,4,5 // 降序排序 myList.sort(std::greater<int>()); // 5,4,3,2,1 // 自定义排序 struct Person { std::string name; int age; }; std::list<Person> people = {{"Alice",25}, {"Bob",30}, {"Charlie",20}}; people.sort([](const Person& a, const Person& b) { return a.age < b.age; });4.2 合并操作
merge()用于合并两个已排序的list:
std::list<int> list1 = {1,3,5}; std::list<int> list2 = {2,4,6}; list1.merge(list2); // list1: 1,2,3,4,5,6; list2为空注意:merge操作后,源list(list2)将为空。
4.3 去重操作
unique()用于删除连续的重复元素:
std::list<int> myList = {1,2,2,3,3,3,2,1,1}; myList.unique(); // 1,2,3,2,1 // 使用自定义谓词 myList.unique([](int a, int b) { return abs(a-b) < 2; // 删除差值小于2的相邻元素 });4.4 拼接操作
splice()用于将一个list的元素移动到另一个list中:
std::list<int> list1 = {1,2,3}; std::list<int> list2 = {4,5,6}; // 将list2所有元素移动到list1末尾 list1.splice(list1.end(), list2); // list1:1,2,3,4,5,6; list2为空 // 移动单个元素 list2 = {7,8,9}; auto it = list2.begin(); list1.splice(list1.begin(), list2, it); // list1:7,1,2,3,4,5,6; list2:8,9 // 移动元素范围 list2 = {10,11,12}; auto first = list2.begin(); auto last = list2.end(); list1.splice(list1.end(), list2, first, last); // list1:7,1,2,3,4,5,6,10,11,125. list的迭代器特性
list的迭代器属于双向迭代器,支持以下操作:
- 递增(++)、递减(--)
- 解引用(*)
- 比较(==, !=)
但不像随机访问迭代器,不支持:
- 算术运算(+, -)
- 关系比较(<, >)
- 下标操作([])
std::list<int> myList = {1,2,3,4,5}; // 正向遍历 for(auto it = myList.begin(); it != myList.end(); ++it) { std::cout << *it << " "; } // 反向遍历 for(auto rit = myList.rbegin(); rit != myList.rend(); ++rit) { std::cout << *rit << " "; } // 注意:以下操作不合法 // auto it = myList.begin() + 2; // 错误! // if(it1 < it2) // 错误!6. list的性能优化与使用场景
6.1 适用场景
- 需要频繁在中间位置插入/删除元素
- 不需要随机访问元素
- 需要稳定的迭代器(插入删除不会使其他元素的迭代器失效)
- 需要高效的合并、排序操作
6.2 性能优化技巧
- 预分配节点:对于已知大小的list,可以先resize()再修改,减少内存分配次数
- 批量操作:尽量使用insert()的范围版本,而非循环push_back
- 排序选择:优先使用成员函数sort()而非std::sort()
- 避免不必要的拷贝:使用emplace操作直接构造元素
struct Point { Point(int x, int y) : x(x), y(y) {} int x, y; }; std::list<Point> points; // 低效 - 构造临时对象再拷贝 points.push_back(Point(1,2)); // 高效 - 直接构造 points.emplace_back(1,2);7. list与其他容器的比较
| 特性 | list | vector | deque |
|---|---|---|---|
| 内部结构 | 双向链表 | 动态数组 | 分块数组 |
| 随机访问 | 不支持 | 支持 | 支持 |
| 中间插入/删除 | O(1) | O(n) | O(n) |
| 末尾插入/删除 | O(1) | O(1)摊销 | O(1) |
| 开头插入/删除 | O(1) | O(n) | O(1) |
| 迭代器失效 | 很少 | 经常 | 中等 |
| 内存使用 | 较高(指针) | 较低 | 中等 |
8. list的实现细节探究
8.1 典型list节点实现
template<typename T> struct __list_node { __list_node* prev; __list_node* next; T data; };8.2 list的end()迭代器
list通常使用一个哨兵节点(sentinel)来表示end()位置,这个节点不存储实际数据,只是作为链表的尾标记。
8.3 内存分配策略
大多数实现会采用内存池技术来优化频繁的小内存分配,减少内存碎片。
9. 实际应用案例
9.1 使用list实现LRU缓存
class LRUCache { private: int capacity; std::list<std::pair<int, int>> cache; std::unordered_map<int, std::list<std::pair<int, int>>::iterator> map; public: LRUCache(int capacity) : capacity(capacity) {} int get(int key) { auto it = map.find(key); if(it == map.end()) return -1; // 移动到链表头部 cache.splice(cache.begin(), cache, it->second); return it->second->second; } void put(int key, int 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(); } };9.2 多线程环境下的list使用
list的迭代器稳定性使其适合在某些多线程场景下使用,但需要注意同步:
std::list<int> sharedList; std::mutex mtx; void producer() { for(int i = 0; i < 100; ++i) { std::lock_guard<std::mutex> lock(mtx); sharedList.push_back(i); } } void consumer() { while(true) { std::lock_guard<std::mutex> lock(mtx); if(!sharedList.empty()) { int val = sharedList.front(); sharedList.pop_front(); // 处理val... } } }10. 常见问题与解决方案
10.1 迭代器失效问题
虽然list的插入删除操作通常不会使迭代器失效,但以下情况需要注意:
- 删除元素会使指向该元素的迭代器失效
- merge、splice等操作会影响迭代器的有效性
安全实践:
std::list<int> myList = {1,2,3,4,5}; auto it = myList.begin(); std::advance(it, 2); // 指向3 // 删除元素前保存下一个迭代器 auto nextIt = std::next(it); myList.erase(it); // it失效,但nextIt仍然有效10.2 性能陷阱
- 线性查找:list的find操作是O(n),对于频繁查找的场景应考虑unordered_set或set
- 错误使用算法:避免在list上使用需要随机访问迭代器的算法,如std::sort
10.3 自定义类型注意事项
当list存储自定义类型时,需要确保类型满足:
- 可拷贝构造/可移动构造
- 如果使用sort(),需要定义比较操作或提供比较函数
struct MyType { int id; std::string name; // 为sort提供比较运算符 bool operator<(const MyType& other) const { return id < other.id; } }; std::list<MyType> items; items.sort(); // 使用operator<排序 // 或者提供自定义比较函数 items.sort([](const MyType& a, const MyType& b) { return a.name < b.name; });11. C++11/14/17对list的增强
11.1 emplace操作
C++11引入了emplace系列方法,支持原地构造元素:
std::list<std::pair<int, std::string>> myList; // C++98方式 myList.push_back(std::make_pair(1, "one")); // C++11方式 myList.emplace_back(1, "one"); // 直接构造11.2 初始化列表
C++11支持使用初始化列表构造list:
std::list<int> myList = {1,2,3,4,5};11.3 非成员函数size()
C++17提供了非成员函数size(),与成员函数功能相同:
std::list<int> myList = {1,2,3}; std::cout << std::size(myList); // 312. 跨平台注意事项
不同STL实现(Microsoft STL, libstdc++, libc++)对list的实现细节可能略有差异,但接口和行为保持一致。需要注意:
- 迭代器失效的具体规则
- 异常安全保证
- 内存使用模式
在编写跨平台代码时,应严格遵循标准规定,避免依赖特定实现行为。