一、list 核心概念
1.1 本质
list 是带头结点的双向循环链表 ,底层节点动态开辟,非连续内存空间 。迭代器是对节点指针的封装(不是原生指针)。
1.2 与 vector 的本质区别
| vector | list | |
|---|---|---|
| 底层结构 | 连续空间(动态数组) | 带头结点的双向循环链表 |
| 迭代器 | 原 |
六、list vs vector 完整对比
| vector | list | |
|---|---|---|
| 底层结构 | 动态数组,连续空间 | 带头结点双向循环链表 |
| 随机访问 | O(1) | O(n) |
| 头插/头删 | O(n),需搬移所有元素 | O(1) |
| 任意位置插入/删除 | O(n) | O(1) |
| 增容代价 | 开辟新空间+拷贝+释放旧空间 | 无增容,每次插入只申请一个节点 |
| 空间利用率 | 高,无额外指针开销 | 低,每个节点额外存两个指针 |
| 缓存利用率 | 高,连续内存预读友好 | 低,节点分散在堆中 |
| 内存碎片 | 不易产生内存碎片 | 小节点频繁申请释放,易产生碎片 |
| 迭代器类型 | 随机访问迭代器 | 双向迭代器 |
| 迭代器底层 | 原生指针 T* |
封装的节点指针 |
| 插入时迭代器失效 | 会(扩容导致) | 不会 |
| 删除时迭代器失效 | 当前及之后全部失效 | 仅被删节点失效 |
operator[] |
支持 | 不支持(无法随机访问) |
reserve/capacity |
有 | 没有 |
| 适用场景 | 高效存储 + 随机访问,少插入删除 | 大量插入删除,不关心随机访问 |
七、复习自测问答
Q1:list 的底层结构是什么?
答: 带头结点的双向循环链表。
Q2:list 的迭代器是原生指针吗?
答: 不是。list 的迭代器是对节点指针的封装 (必须支持 ++、-- 等操作跳转到相邻节点,原生指针做不到),而 vector 的迭代器才是原生指针 T*。
Q3:list 插入元素会导致迭代器失效吗?
答: 不会。因为链表插入只改指针指向,不搬移已有节点,也不扩容。
Q4:list 删除元素会导致哪些迭代器失效?
答: 只有被删除节点的迭代器失效,其他节点的迭代器不受影响。
Q5:list 有 reserve 和 capacity 吗?为什么?
答: 没有。list 是非连续空间,每个节点独立申请内存,不需要预留空间,也没有"容量"的概念。
Q6:list 支持 operator[] 随机访问吗?
答: 不支持。底层是链表,访问第 n 个元素需要 O(n) 遍历。只能用迭代器或 range-for。
Q7:list 的 end() 指向哪里?
答: 指向头结点 (哨兵节点),不是最后一个有效元素。front() 返回头结点下一个节点的值,back() 返回头结点上一个节点的值。
Q8:反向迭代器如何实现?
答: 内部包含一个正向迭代器。反向 ++ 调用正向 --,反向 -- 调用正向 ++。operator* 返回前一个位置的值,保证区间对称。
Q9:什么时候用 list,什么时候用 vector?
答:
- list :大量任意位置插入/删除,不关心随机访问
- vector :需要随机访问、高效存储、关心缓存效率,插入删除较少
- 实际中 vector 的使用频率远高于 list(CPU 缓存优势)
Q10:push_back 在 list 和 vector 中的复杂度分别是多少?
答: list 是 O(1) (只改两个指针)。vector 是均摊 O(1)(可能触发扩容,单次最坏 O(n))。
复习建议:重点掌握 list 与 vector 的底层区别、迭代器失效规则差异、以及各自的适用场景。第三、六章是面试高频考点。
生指针
T*| 对节点指针的封装 || 随机访问 | O(1) | O(n) |
| 任意位置插入删除 | O(n),需搬移元素 | O(1),只改指针 |
二、核心接口速查
2.1 构造函数
| 构造函数 | 说明 |
|---|---|
list<T> l; |
空的 list |
list<T> l(n, val); |
n 个 val |
list<T> l(first, last); |
迭代器区间构造 |
list<T> l(const list& x); |
拷贝构造 |
2.2 迭代器
| 接口 | 说明 |
|---|---|
begin() / end() |
正向,++ 向后移动 |
rbegin() / rend() |
反向,++ 向前移动(rbegin 即 end 位置,rend 即 begin 位置) |
2.3 容量与元素访问
| 接口 | 说明 |
|---|---|
empty() |
判空 |
size() |
有效节点个数 |
front() |
第一个元素值的引用 |
back() |
最后一个元素值的引用 |
2.4 增删改
| 接口 | 说明 | 复杂度 |
|---|---|---|
push_front(val) |
头插 | O(1) |
pop_front() |
头删 | O(1) |
push_back(val) |
尾插 | O(1) |
pop_back() |
尾删 | O(1) |
insert(pos, val) |
在 pos 前插入 | O(1) |
erase(pos) |
删除 pos 位置节点 | O(1) |
swap(l1, l2) |
交换两个 list | O(1) |
clear() |
清空所有有效元素 | O(n) |
注意:
find不是 list 的成员函数,是<algorithm>中的算法。
三、list 迭代器失效(重点!与 vector 不同)
3.1 核心规则
| 操作 | 迭代器是否失效 |
|---|---|
| 插入(insert) | 不会失效(链表不搬移节点,只改指针) |
| 删除(erase) | 只有被删除节点的迭代器失效,其他迭代器不受影响 |
| resize / reserve | list 没有这两个接口(非连续空间,无 capacity 概念) |
3.2 删除时的安全写法
错误写法:
cpp
auto it = l.begin();
while (it != l.end()) {
l.erase(it); // it 指向的节点被删除,it 失效
++it; // 失效的 it 再 ++ 崩溃
}
正确写法一(后置 ++):
cpp
auto it = l.begin();
while (it != l.end()) {
l.erase(it++); // it++ 先返回旧值,再指向下一个,erase 删除旧值
}
正确写法二(用返回值):
cpp
auto it = l.begin();
while (it != l.end()) {
it = l.erase(it); // C++11 起 erase 返回下一个有效迭代器
}
3.3 list vs vector 迭代器失效对比(面试高频)
| 场景 | vector | list |
|---|---|---|
| 插入元素 | 可能全部失效(扩容导致空间重分配) | 永不失效 |
| 删除元素 | 当前及之后全部失效(元素前移) | 仅被删节点失效 |
| 失效原因 | 底层连续空间被释放或元素搬移 | 节点被删除,指针变野指针 |
四、反向迭代器实现原理
4.1 核心思想
反向迭代器内部包含一个正向迭代器,通过对正向迭代器进行包装实现:
- 反向迭代器的
++= 正向迭代器的-- - 反向迭代器的
--= 正向迭代器的++
4.2 解引用的对称设计
cpp
// operator*:返回的是 _it 前一个位置的值
Ref operator*() {
Iterator temp(_it);
--temp;
return *temp;
}
原因 :rbegin() 内部存的是正向 end(),需要 -- 后才指向最后一个有效元素。这是为了保证开始位置和结束位置对称 ,使得 [rbegin(), rend()) 的区间完整覆盖所有元素。
4.3 typename 关键字的作用
cpp
typedef typename Iterator::Ref Ref;
告诉编译器 :Iterator::Ref 是一个类型而不是静态成员变量,否则编译器无法区分。
五、list 模拟实现要点
- 带头结点的双向循环链表 :头结点不存数据,
end()指向头结点 - 节点结构 :包含
_prev、_next指针和_data - insert:在指定位置前插入新节点,只改 4 个指针
- erase:删除指定节点,只改 2 个指针,返回下一节点
- 迭代器类 :需重载
operator*、operator->、operator++、operator--、operator==、operator!=