1. C++ STL中的list容器概述
在C++标准模板库(STL)中,list是一个双向链表容器,它允许在常数时间内进行任意位置的插入和删除操作。与vector这种连续存储的容器不同,list的元素在内存中是非连续存储的,每个元素都包含指向前驱和后继节点的指针。
list特别适合以下场景:
- 需要频繁在序列中间插入/删除元素
- 不需要随机访问元素(即不需要通过下标直接访问)
- 需要保证迭代器在插入/删除操作后不失效
2. list的基本操作与使用
2.1 创建和初始化list
list容器提供了多种初始化方式:
cpp复制#include <list>
using namespace std;
// 空list
list<int> list1;
// 包含5个元素,每个元素值为0
list<int> list2(5);
// 包含5个元素,每个元素值为42
list<int> list3(5, 42);
// 通过初始化列表
list<int> list4 = {1, 2, 3, 4, 5};
// 通过其他容器的迭代器范围
vector<int> vec = {6, 7, 8};
list<int> list5(vec.begin(), vec.end());
2.2 常用成员函数
list提供了一系列成员函数来操作链表:
cpp复制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();
// 检查是否为空
bool isEmpty = myList.empty();
// 访问第一个和最后一个元素
int first = myList.front();
int last = myList.back();
3. list的高级特性
3.1 迭代器操作
list支持双向迭代器,可以向前或向后遍历:
cpp复制list<int> nums = {1, 2, 3, 4, 5};
// 正向遍历
for(auto it = nums.begin(); it != nums.end(); ++it) {
cout << *it << " ";
}
// 反向遍历
for(auto it = nums.rbegin(); it != nums.rend(); ++it) {
cout << *it << " ";
}
// 使用基于范围的for循环(C++11)
for(int num : nums) {
cout << num << " ";
}
3.2 插入和删除操作
list的插入和删除操作非常高效:
cpp复制list<int> myList = {10, 20, 30};
// 在指定位置前插入元素
auto it = myList.begin();
advance(it, 1); // 移动到第二个元素
myList.insert(it, 15); // 10,15,20,30
// 删除指定位置的元素
it = myList.begin();
advance(it, 2);
myList.erase(it); // 10,15,30
// 删除所有值为15的元素
myList.remove(15); // 10,30
// 删除满足条件的元素
myList.remove_if([](int n){ return n > 25; }); // 10
3.3 list特有的操作
list提供了一些vector没有的操作,这些操作利用了链表的特性:
cpp复制list<int> list1 = {1, 2, 3};
list<int> list2 = {4, 5, 6};
// 合并两个有序list(两个list都必须已排序)
list1.sort();
list2.sort();
list1.merge(list2); // list1: 1,2,3,4,5,6; list2为空
// 排序list
list1 = {3, 1, 4, 2};
list1.sort(); // 1,2,3,4
// 反转list
list1.reverse(); // 4,3,2,1
// 去除连续重复元素(需要先排序)
list1.sort();
list1.unique();
4. list的性能分析与使用建议
4.1 时间复杂度分析
- 插入/删除:O(1)(已知位置)
- 访问元素:O(n)(需要遍历)
- 排序:O(n log n)(使用成员函数sort)
- 查找:O(n)
4.2 何时使用list
list在以下情况下是更好的选择:
- 需要频繁在序列中间插入/删除元素
- 需要保证插入/删除操作不会使迭代器失效
- 不需要随机访问元素
- 需要高效地在两端插入/删除元素
4.3 与vector和deque的比较
| 特性 | vector | deque | list |
|---|---|---|---|
| 存储方式 | 连续 | 分块连续 | 非连续 |
| 随机访问 | O(1) | O(1) | O(n) |
| 头部插入/删除 | O(n) | O(1) | O(1) |
| 中间插入/删除 | O(n) | O(n) | O(1) |
| 尾部插入/删除 | O(1) | O(1) | O(1) |
| 迭代器失效 | 可能 | 可能 | 不会 |
| 内存使用 | 低 | 中等 | 高 |
5. 实际应用示例
5.1 实现LRU缓存
list常被用于实现LRU(最近最少使用)缓存算法:
cpp复制class LRUCache {
private:
int capacity;
list<pair<int, int>> cache;
unordered_map<int, list<pair<int, int>>::iterator> map;
public:
LRUCache(int capacity) : capacity(capacity) {}
int get(int key) {
if(map.find(key) == map.end()) return -1;
// 移动到链表头部
cache.splice(cache.begin(), cache, map[key]);
return map[key]->second;
}
void put(int key, int value) {
if(map.find(key) != map.end()) {
// 更新值并移动到头部
map[key]->second = value;
cache.splice(cache.begin(), cache, map[key]);
return;
}
if(cache.size() == capacity) {
// 删除尾部元素
int lastKey = cache.back().first;
map.erase(lastKey);
cache.pop_back();
}
// 插入新元素到头部
cache.emplace_front(key, value);
map[key] = cache.begin();
}
};
5.2 处理大型数据集的中间插入
当需要处理大量数据且频繁在中间插入时,list比vector更高效:
cpp复制// 生成100万个随机数
list<int> bigData;
for(int i = 0; i < 1000000; ++i) {
bigData.push_back(rand());
}
// 在中间位置插入1000个元素
auto it = bigData.begin();
advance(it, 500000); // 移动到中间位置
for(int i = 0; i < 1000; ++i) {
bigData.insert(it, i); // 在中间插入
}
6. 性能优化与注意事项
6.1 避免频繁的advance操作
虽然list的插入删除是O(1),但找到插入位置可能需要O(n)时间:
cpp复制// 不推荐 - 每次循环都执行O(n)的advance
for(int i = 0; i < 1000; ++i) {
auto it = myList.begin();
advance(it, 500); // 每次都要从头开始遍历
myList.insert(it, i);
}
// 推荐 - 只遍历一次
auto it = myList.begin();
advance(it, 500);
for(int i = 0; i < 1000; ++i) {
myList.insert(it, i); // 插入位置不变
}
6.2 使用splice高效转移元素
splice可以在常数时间内将元素从一个list转移到另一个list:
cpp复制list<int> list1 = {1, 2, 3};
list<int> list2 = {4, 5, 6};
// 将list2的所有元素转移到list1的末尾
list1.splice(list1.end(), list2);
// 现在list1: 1,2,3,4,5,6; list2为空
6.3 自定义排序
list的sort成员函数可以接受自定义比较函数:
cpp复制list<string> names = {"Alice", "Bob", "Charlie"};
// 按长度排序
names.sort([](const string& a, const string& b) {
return a.length() < b.length();
});
6.4 内存使用考虑
list的每个元素都需要额外的内存存储前后指针(在64位系统上通常是16字节额外开销)。对于小型元素,这可能导致显著的内存 overhead。例如,存储100万个int:
- vector: ~4MB
- list: ~24MB (4字节数据 + 16字节指针)
7. C++11/14/17对list的增强
7.1 初始化列表
C++11引入了初始化列表语法,使list初始化更简洁:
cpp复制list<int> myList = {1, 2, 3, 4, 5}; // C++11
7.2 emplace操作
emplace系列函数允许直接在容器中构造元素,避免临时对象:
cpp复制list<pair<int, string>> myList;
myList.emplace_back(42, "answer"); // 直接在容器中构造pair
7.3 基于范围的for循环
简化遍历操作:
cpp复制for(const auto& item : myList) {
cout << item << endl;
}
7.4 C++17的splice重载
C++17为splice添加了新的重载,可以转移单个元素:
cpp复制list<int> list1 = {1, 2, 3};
list<int> list2 = {4, 5, 6};
// 将list2的第二个元素转移到list1末尾
auto it = list2.begin();
advance(it, 1);
list1.splice(list1.end(), list2, it);
