先别急着背"双向链表"。先回答一个问题:
std::sort(lt.begin(), lt.end())为什么编译不过?这篇我尽量说人话,少堆术语。先把"为什么"讲明白,再看 list 长什么样、怎么用、怎么手写。
开篇
学习方式还是老样子:听课 + 自己敲 + AI 辅助。这篇对应第 17 课(5.27 list),代码在 VS 2026(MSVC 14.51)下编译运行。
先给结论:
list 是双向链表,迭代器只能
++/--,不能+n;std::sort要随机访问迭代器,所以用不了。list 自己提供了成员sort()。
一、先看现象:vector 能 sort,list 不能
cpp
vector<int> v = {3, 1, 2};
sort(v.begin(), v.end()); // OK
list<int> lt = {3, 1, 2};
sort(lt.begin(), lt.end()); // 编译不过
lt.sort(); // OK,list 自己的成员函数
这不是"list 不能排序",而是 std::sort 要求随机访问迭代器,list 给不了。

你可以把迭代器想成插头:
find要两孔插头(输入迭代器);reverse要三孔(双向);std::sort要更高级的插头(随机访问)。
list 的迭代器是 bidirectional :能 ++、--,但不能 it + n、不能 it[n]。所以:
reverse(lt.begin(), lt.end())可以;find、remove可以;std::sort不行,list 用成员sort()。
二、list 长什么样:一个带哨兵头的环

list 的底层是带头双向循环链表:
cpp
_head -> 1 <-> 2 <-> 3 <-+
^ |
+------------------------+
_head是哨兵节点,不存有效数据;begin()返回_head->_next;end()返回_head;- 空 list 时,
_head->_next == _head->_prev == _head,所以begin() == end()。
为什么要有 _head?打个比方:它就像地铁环线的调度站。环线没有"起点/终点",但调度站让你知道从哪儿开始、到哪儿结束。没有它,你只有一个环,很难定义 begin() 和 end()。
三、push_back 四步:别死记,画图

cpp
void push_back(const T& x)
{
Node* newnode = new Node(x);
Node* tail = _head->_prev; // 先找尾
tail->_next = newnode; // 1. 尾的后继指向新节点
newnode->_prev = tail; // 2. 新节点的前驱指向尾
newnode->_next = _head; // 3. 新节点的后继指向头
_head->_prev = newnode; // 4. 头的前驱指向新节点
}
四步缺一不可:
- 少改
newnode->_next,从新节点往后走就断了; - 少改
_head->_prev,下次尾插就找不到真正的尾。
四、迭代器为什么要包一层?

list 的迭代器不是 Node*,而是:
cpp
template<class T>
struct __list_iterator
{
Node* _node;
T& operator*() { return _node->_data; }
__list_iterator<T>& operator++() { _node = _node->_next; return *this; }
bool operator!=(const __list_iterator<T>& it) { return _node != it._node; }
};
为什么不直接用 Node*?
*Node*得到的是整个节点,不是_data;++Node*是指针算术,跑到下一个节点对象的位置,但节点又不在连续数组里,直接乱套。
所以要用一个类把 Node* 包起来,重载 *、++、!=,让它的行为像"指向元素"的迭代器。
范围 for 能跑,靠的就是这三样:begin()/end()、!=、*、++。
五、list 的常用操作:merge 和 splice 别混

cpp
list<int> a = {1, 3, 5};
list<int> b = {2, 4, 6};
a.merge(b); // 前提:a、b 都已排序;合并后 b 为空
list<int> l = {1, 2, 3, 4, 5};
auto it = find(l.begin(), l.end(), 4);
l.splice(l.begin(), l, it); // 把 4 挪到头部;不排序也能用
l.remove(2); // 删除所有 2
l.sort(); // 成员排序
这里最容易混:
- merge :合并两个有序链表;不拷贝元素,只转移节点;
- splice :把节点"剪"过去;不要求有序;不拷贝元素;
- 两者转移后,被转移元素的迭代器仍然有效,只是现在属于目标链表。
我一开始以为 splice 也要先排序,这是错的。splice 就是纯改指针,跟排序没关系。

六、为什么 vector 排序常常比 list 快?

复杂度都是 O(n log n),但常数差很多:
- vector 元素连续:一个 cache line 装多个,硬件预取友好,排序时交换/移动都在连续内存里;
- list 节点分散:每次
++都要跟着指针跳到另一个堆地址,cache miss 多,还有分配器开销。
所以 不是 list 的归并排序不行,是它每次"找下一个"都要跳内存。
list 适合:
- 频繁在中间插入/删除,并且已经拿到位置迭代器;
- 需要迭代器稳定,插入/删除不影响其他节点;
- 不需要随机访问、不需要排序。
vector 适合:
- 随机访问、排序、连续遍历;
- 尾插为主,偶尔扩容。
七、手写 list:目前只写了个开头
list.h 这版实现了:
list_node:_data、_next、_prev;__list_iterator:*、++、!=;list:哨兵_head、begin/end、push_back。
还缺:
const_iterator、begin() const;- 析构、
insert、erase、push_front、pop_back; size()、clear()、拷贝构造/赋值。
所以现在写 const list<int> lt; lt.begin(); 是调不通的,因为 begin() 不是 const 成员。下一课继续补。
八、三句话总结
- list 是双向链表,迭代器是 bidirectional :能
++/--,不能+n,所以std::sort用不了,得用成员sort()。 _head是哨兵 :begin()是_head->_next,end()是_head;空表时两者相等。- merge 要排序,splice 不用;两者都不拷贝元素,被转移元素的迭代器仍然有效。
我踩过的坑
| 我一开始以为 | 实际 |
|---|---|
| list 不能排序 | 是不能用 std::sort;list 有成员 sort() |
| splice 也要先排序 | splice 不要求有序,merge 才要求 |
| splice 后迭代器全失效 | 被转移元素的迭代器仍然有效 |
| list sort 慢是算法差 | 主因是节点分散、cache miss |
const list 也能调 begin() |
现在没写 begin() const,调不了 |
这篇就到这里。下一篇继续把 list 的 const_iterator、insert/erase 和析构补完。