线性家族的另一半纪律是 只从哪一端进出。Python 标准库不提供教科书式单链表类型;生产路径是:
| 纪律 | 用这个 | 不要用 |
|---|---|---|
| 栈(stack) 后进先出 | list.append / list.pop |
头插 insert(0, …) |
| 队列(queue) 先进先出 | collections.deque 的 append / popleft |
list.pop(0) |
| 双端队列(double-ended queue,deque) | 同一个 deque 两头都动 |
手写双向节点当默认 |
段末注释:栈 / 队列 是 ADT(谁能进出),链表 是实现。CPython 的
deque不是「一节点一元素」的链表,而是双向串起来的 64 槽指针块。

1. 为什么日常几乎不写链表
已知节点处插入删除是 (O(1)),但「找到那个节点」在 Python 里通常已经 (O(n))。每个节点一次堆分配、两次指针跳转,缓存极差。CPython 自己实现队列时,选择 块状双向链表:每块 64 个 PyObject*,块与块之间才有 prev / next。
1 | deque |
两端 append / pop 均摊 (O(1)),且不 realloc 整块数据。maxlen 满员时从对面丢掉,用来做滑动窗口。
2. 栈:list 就够
1 | def eval_rpn(tokens): |
递归调用栈也是栈,只是你看不见。显式 list 当栈的好处是可控、可迭代、不会撞递归上限。
线程间的栈用 queue.LifoQueue(内部有锁),不要多线程共用一个裸 list。
3. 队列与双端队列:deque
1 | from collections import deque |
| 操作 | deque |
list |
|---|---|---|
| 右端进/出 | (O(1)) | (O(1)) |
| 左端进/出 | (O(1)) | (O(n)) |
| 按下标 | (O(n))(要跨块走) | (O(1)) |
| 按值查找 | (O(n)) | (O(n)) |
deque 不是数组:不要用它当随机访问容器。d[0] / d[-1] 可以,中间下标会慢。
线程安全队列:queue.Queue(FIFO)、queue.PriorityQueue(堆 + 锁)。单线程算法题不要用它们,锁是纯开销。
4. 手写链表:只为把指针讲清楚
生产代码默认不要走这条路。面试或要在已知节点 (O(1)) 摘链时才写。
1 | class Node: |
双向节点多一个 prev,删除当前节点不必找前驱。循环链表让尾的 next 指回头,用来做约瑟夫环或环形缓冲的教学模型;工程环形缓冲用 deque(maxlen=…) 或一块 bytearray。
5. 组合体:哈希表 + 双向链表 = LRU
最近最少使用(Least Recently Used,LRU) 要同时:按键 (O(1)) 找到、(O(1)) 把该节点挪到「最新」端。标准库现成方案:
1 | from collections import OrderedDict |
函数结果缓存直接 @functools.lru_cache,不要自己造。OrderedDict 在 3.7+ 的普通 dict 已保序之后,仍多了 move_to_end / popitem(last=),这才是它留下的理由。
6. 踩坑
list当队列:pop(0)总复杂度 (O(n^2))。- 多线程共用
deque:deque两端操作大致原子,但「先看长度再 pop」不是原子。跨线程用queue.Queue。 - 在
deque中间insert:合法但 (O(n)),失去选它的意义。 - 手写链表当默认容器:分配与缓存成本通常大于
list搬指针。 - 递归当队列:递归是栈,宽搜必须显式队列。
7. 自检
- 用一张表说出栈 / 队列 / deque 在 CPython 里各用谁实现。
deque为什么比「一元素一节点」快?块大小是多少?- 什么情况下值得手写
Node? - LRU 为什么必须「哈希 + 双向链(或 OrderedDict)」两样一起上?
8. 社区口径与风险
队列实现见 CPython Modules/_collectionsmodule.c(BLOCKLEN = 64)。算法题社区默认 from collections import deque。第三方几乎不必为「普通队列」再引包。风险:把 Java LinkedList 的用法直接翻译成 Python 手写链,常数会差一个数量级。