C++ 入门笔记(十七):list 入门 —— 为什么 list 不能 sort?

先别急着背"双向链表"。先回答一个问题: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 成员。下一课继续补。

八、三句话总结

  1. list 是双向链表,迭代器是 bidirectional :能 ++/--,不能 +n,所以 std::sort 用不了,得用成员 sort()。
  2. _head 是哨兵 :begin() 是 _head->_next,end() 是 _head;空表时两者相等。
  3. 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 和析构补完。

相关推荐
青少儿编程课堂1 小时前
蓝桥杯青少组图形化 + 全国青少年信息素养大赛 + 电子学会Scratch考级:校园连连看解析
c++·python·算法·bfs·信息学竞赛
Rain的Java大神之路2 小时前
设计一套生产级 API 接口:从契约、安全、幂等到限流,我踩过的坑都在这了
java·后端·面试
草莓熊Lotso2 小时前
【C++ 基于微服务的即时通讯系统】文件存储子服务实战:UUID生成、文件IO封装与全接口单元测试
开发语言·网络·数据库·c++·微服务·架构·单元测试
zaizai10073 小时前
算法(左程云)1
数据结构
小南家的青蛙3 小时前
微服务体系下DevOps运维岗位面试方案设计
运维·面试·职场和发展·devops
charliejohn3 小时前
计算机考研 408 数据结构 图论-关键路径
数据结构·图论
聪明的笨猪猪17 小时前
实际工程问题:AI 生成 Spring Cache(@Cacheable / @CachePut / @CacheEvict)缓存注解踩坑
java·经验分享·面试·ai编程
David猪大卫19 小时前
【C++修炼】map 和 set 的使用
c语言·开发语言·数据结构·c++·经验分享·笔记·学习