list学习总结
list 学习总结
std::list 是 C++ 标准库中最常用的链表容器,本文从零到熟练梳理它的所有核心用法。
1. list 是什么
- 存储不连续内存的元素序列,节点之间通过指针相连。
- 底层是双向链表:每个节点有两个指针,分别指向前驱和后继。
- 任意位置插入、删除都是 O(1)(只需要改指针,不需要移动元素)。
- 不支持随机访问:不能按下标 O(1) 访问,只能从头部或尾部逐个遍历。
- 属于"序列容器",头文件
<list>。
#include <list>
list<int> l; // 空 list,int 类型
对比记忆:vector 是"连续内存的动态数组",访问快、尾部增删快;list 是"分散内存的链表",任意位置增删快但访问慢;map 是"按 key 组织成树"的关联容器。
2. 定义与初始化
#include <list>
#include <string>
using namespace std;
// 1. 空 list
list<int> l1;
// 2. 指定大小
list<int> l2(5); // 5 个元素,都是 0
list<int> l3(5, 42); // 5 个元素,都是 42
// 3. 初始化列表
list<int> l4{1, 2, 3, 4, 5};
// 4. 从其他容器构造
vector<int> v{5, 4, 3, 2, 1};
list<int> l5(v.begin(), v.end()); // {5, 4, 3, 2, 1}
// 5. 拷贝构造
list<int> l6(l4);
// 6. 移动构造(C++11)
list<int> l7(std::move(l4)); // l4 被掏空
3. 核心操作一览
| 操作 | 写法 | 说明 | 复杂度 |
|---|---|---|---|
| 头部插入 | l.push_front(x) |
在头部添加元素 | O(1) |
| 头部插入 | l.emplace_front(args...) |
C++11,原地构造 | O(1) |
| 尾部插入 | l.push_back(x) |
在末尾添加元素 | O(1) |
| 尾部插入 | l.emplace_back(args...) |
C++11,原地构造 | O(1) |
| 中间插入 | l.insert(it, x) |
在迭代器 it 前插入 | O(1) |
| 头部删除 | l.pop_front() |
删除第一个元素 | O(1) |
| 尾部删除 | l.pop_back() |
删除最后一个元素 | O(1) |
| 中间删除 | l.erase(it) |
删除迭代器 it 处元素,返回下一个迭代器 | O(1) |
| 范围删除 | l.erase(first, last) |
删除 [first, last) 范围内元素 | O(n) |
| 按值删除 | l.remove(x) |
删除所有值等于 x 的元素 | O(n) |
| 条件删除 | l.remove_if(pred) |
删除所有满足谓词的元素 | O(n) |
| 去重 | l.unique() |
删除相邻重复元素 | O(n) |
| 排序 | l.sort() |
成员函数排序 | O(n log n) |
| 反转 | l.reverse() |
反转整个 list | O(n) |
| 拼接 | l.splice(pos, other) |
把 other 的节点搬进 l(不拷贝) | O(1) |
| 合并 | l.merge(other) |
合并两个有序 list | O(n) |
| 首元素 | l.front() |
返回第一个元素的引用 | O(1) |
| 尾元素 | l.back() |
返回最后一个元素的引用 | O(1) |
| 大小 | l.size() |
元素个数 | O(1) |
| 清空 | l.clear() |
删除所有元素 | O(n) |
| 判空 | l.empty() |
size == 0 返回 true | O(1) |
| 交换 | l.swap(other) |
交换两个 list 的内容 | O(1) |
注意:list 没有 operator[]、at()、data(),不支持随机访问!
4. 增删查改实战
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
int main() {
list<int> l;
// ===== 插入 =====
l.push_back(30);
l.push_front(10); // {10, 30}
l.push_back(40); // {10, 30, 40}
l.emplace_back(50); // {10, 30, 40, 50}
auto it = l.begin();
advance(it, 2); // 找到位置(list 迭代器只能 ++/--)
l.insert(it, 20); // 在 30 前插入 20 -> {10, 20, 30, 40, 50}
// ===== 访问 =====
cout << l.front() << endl; // 10
cout << l.back() << endl; // 50
// ===== 查找(list 没有内置 find,用 std::find) =====
auto pos = find(l.begin(), l.end(), 30);
if (pos != l.end()) {
cout << "Found: " << *pos << endl; // Found: 30
}
// ===== 遍历 =====
// 1. 迭代器遍历(不支持下标)
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 10 20 30 40 50
}
cout << endl;
// 2. 范围 for(C++11)
for (auto& x : l) {
cout << x << " ";
}
cout << endl;
// ===== 删除 =====
l.pop_front(); // 删除头部 {20, 30, 40, 50}
l.pop_back(); // 删除尾部 {20, 30, 40}
l.remove(30); // 按值删除 {20, 40}
l.push_back(4);
l.push_back(6);
l.remove_if([](int x) { return x % 2 == 0; }); // 删除所有偶数
// l: { }(20, 40, 4, 6 全被删除)
l.clear(); // 清空
cout << l.size() << endl; // 0
return 0;
}
5. 迭代器的特点(重点)
- list 的迭代器是双向迭代器:只支持
++/--,不支持it + n(vector 才可以)。 - 想跳 n 步必须循环
++n 次,或用std::advance/std::next:
list<int> l{1, 2, 3, 4, 5};
auto it = l.begin();
// advance(it, 2); // it 指向 3
// auto it2 = next(it, 2); // 3 往后两个 -> 5
// 危险:list<int>::iterator 不能 it + 2!
// auto it3 = it + 2; // 编译错误
迭代器稳定性(list 最大的优势):
- 插入元素不会使任何已有迭代器失效。
- 删除元素只会使指向被删除元素的迭代器失效。
- vector 的
insert/erase会让后面的迭代器全部失效;list 完全不会。
list<int> l{1, 2, 3, 4, 5};
auto it3 = find(l.begin(), l.end(), 3);
l.insert(it3, 100); // 插入后 it3 仍然有效,还指向 3
l.push_back(6); // it3 依然有效
l.erase(it3); // 现在只有 it3 失效,其它(包括 100)不受影响
6. 排序:成员函数 sort()(重点)
list 不能使用 std::sort(),因为 std::sort 需要随机访问迭代器(it + n)。list 只能用成员函数 sort():
list<int> l{3, 1, 4, 1, 5, 9};
l.sort(); // 升序:{1, 1, 3, 4, 5, 9}
// 降序(自定义比较器)
l.sort(greater<int>()); // {9, 5, 4, 3, 1, 1}
// 自定义比较器:按绝对值
l.sort([](int a, int b) { return abs(a) < abs(b); });
为什么 std::sort 不能用?
sort(l.begin(), l.end()); // 编译错误!list 的迭代器不支持 it + n
std::sort 内部依赖 first + n、last - n 这样的随机访问操作,list 的双向迭代器不支持。所以标准库专门为 list 提供了成员 sort()。
merge 合并两个有序 list:
list<int> a{1, 3, 5};
list<int> b{2, 4, 6};
a.merge(b); // 两个都升序才能 merge,结果 {1, 2, 3, 4, 5, 6}
// b 被清空(节点被搬走)
7. splice:高效的节点搬移(list 独有的核心能力)
splice 把一个 list 的节点搬到另一个 list,不拷贝元素,纯指针操作 O(1):
list<int> a{1, 2, 3, 4, 5};
list<int> b{7, 8, 9};
// 把 b 的全部节点搬到 a 的开头
a.splice(a.begin(), b); // a: {7, 8, 9, 1, 2, 3, 4, 5}, b 为空
// 把 a 的一个元素搬回 b 的头部
auto it = find(a.begin(), a.end(), 3);
b.splice(b.begin(), a, it); // b: {3}, a: {7, 8, 9, 1, 2, 4, 5}
// 把 a 的一个区间搬到 b 的尾部
auto lo = find(a.begin(), a.end(), 1);
auto hi = next(lo, 3); // 范围 [1, 2, 4)
b.splice(b.end(), a, lo, hi); // b: {3, 1, 2, 4}
vector 做不到这件事:vector 的"拼接"是拷贝元素 O(n),而且会让迭代器失效。
8. unique 去重
注意:unique() 只能删除相邻的重复元素! 使用前必须先 sort():
list<int> l{1, 1, 2, 2, 3, 1, 2};
l.unique(); // 只去相邻重复:{1, 2, 3, 1, 2} —— 最后的 1, 2 还留着!
// 因为没有先排序
l.sort(); // {1, 1, 1, 2, 2, 2}
l.unique(); // {1, 2} ✓ 真正的去重
unique 也支持自定义谓词,删除"满足条件"的相邻元素:
list<string> l{"a", "AB", "b", "CD", "c"};
l.unique([](const string& x, const string& y) {
return tolower(x[0]) == tolower(y[0]); // 忽略大小写去重
});
9. list 与 vector、deque、forward_list 的对比
list |
vector |
deque |
forward_list |
|
|---|---|---|---|---|
| 底层 | 双向链表 | 动态数组 | 分段数组 | 单向链表 |
| 内存连续 | 否 | 是 | 否(分段) | 否 |
| 随机访问 | 不支持 | O(1) | O(1) | 不支持 |
| 头部增删 | O(1) | O(n) | O(1) | O(1) |
| 尾部增删 | O(1) | O(1) 均摊 | O(1) | 无 push_back |
| 中间增删 | O(1)(已有迭代器) | O(n) | O(n) | O(1)(已有迭代器) |
| 迭代器类型 | 双向 | 随机访问 | 随机访问 | 单向 |
| 迭代器稳定性 | 插入不影响,删除只影响被删 | 插入/删除可能全部失效 | 头部尾部插入不影响 | 同 list |
| 额外能力 | splice/merge/remove | reserve/[]/at/data | push_front | splice_after/remove |
选择建议:
- 默认选择 vector:大多数场景 vector 最快(缓存友好)。
- 中间频繁增删且不需要随机访问 →
list,但注意内存开销和低速遍历。 - 只需要单向遍历、最小化内存 →
forward_list(C++11)。 - 频繁头部增删 →
deque。
10. 常见坑汇总
- 没有
operator[]:list 不支持随机访问,不能l[0]。 - 不能直接用
std::sort:必须用成员函数l.sort()。 - 迭代器不支持
it + n:需要std::advance/std::next,或用std::distance求距离。 unique只去相邻重复:去重前必须先sort。remove是 O(n):它必须遍历整个 list 找到所有匹配的元素。- 内存开销大:每个元素额外存 2 个指针(前驱+后继),加上节点分配开销。
- 遍历慢:节点内存不连续,每次访问都可能缓存未命中。大 list 的遍历比 vector 慢一个数量级。
splice会搬走源 list 的节点:用完后源 list 对应的元素就没了。merge要求两个 list 有序:用 undefined behavior 如果没排序。advance跳过整个 list:中间插入需要 O(n) 找到位置(虽然插入本身 O(1))。- list 排序慢:虽然
sort()是 O(n log n),但实际是链表归并排序,常数大,比 vector 排序慢很多。
11. 实战:LRU 缓存模型(list 最经典的场景)
LRU(最近最少使用)缓存需要"移动已有节点到头部",这正是 list 的 splice 擅长的:
#include <iostream>
#include <list>
#include <unordered_map>
using namespace std;
// 简化版 LRU:只演示 list 的核心用法
int main() {
list<int> history; // 访问历史,最近访问的在前
unordered_map<int, list<int>::iterator> pos; // 元素 -> list 中位置
auto access = [&](int x) {
if (pos.count(x)) {
// 已存在:把节点搬到头部(O(1) 指针操作)
history.splice(history.begin(), history, pos[x]);
} else {
// 不存在:插入头部
history.push_front(x);
pos[x] = history.begin();
}
};
access(1); // history: {1}
access(2); // history: {2, 1}
access(3); // history: {3, 2, 1}
access(2); // history: {2, 3, 1} —— 2 被移到头部
access(1); // history: {1, 2, 3}
for (auto& x : history) cout << x << " "; // 1 2 3
cout << endl;
return 0;
}
如果换成 vector 实现同样的功能:每次把元素移到头部都要 O(n) 移动内存,LRU 命中就退化成 O(n)。
12. 实战场景
给定一个需求:需要维护一组数据,频繁在任意位置增删元素,不要求随机访问。
方法一:使用std::vector建模,但中间插入/删除 O(n),频繁操作性能差。
方法二:使用std::list建模,只要有迭代器,任意位置插入/删除都是 O(1),迭代器不会因其它操作失效。
方法三:使用std::deque建模,头尾 O(1),但中间操作仍然是 O(n)。
总结:
1、频繁中间增删且不需要随机访问,选择 list
2、需要拼接(splice)、归并(merge)等操作,只有链表能 O(1) 完成
3、LRU 缓存、"移动已有节点到头部"这类操作必须用 list
4、只是尾部增删 + 随机访问,选 vector(缓存友好,更快)
5、默认不要选 list,它在大多数场景都比 vector 慢(内存开销 + 缓存不命中)
6、list 的迭代器是双向迭代器,只支持 ++/--;vector 的迭代器是随机访问的,支持 it+n
7、list 的迭代器稳定性是它最大的优势:插入不影响已有迭代器,删除只影响被删的那个
另外 list 节点分散在堆上,遍历时需要跳内存访问,速度远慢于 vector 的连续存储

浙公网安备 33010602011771号