“所有度数在0,…,n−1。建立head[d]指向度为d的顶点链表,每点保存prev、next;已知顶点位置后,可在常数时间从旧桶摘除并插入新桶。这里用两个方向的位置数组实现双链,不在线性表里…”
形式陈述
单链表需要维护什么
普通非循环单链表的节点至少有数据 value 和后继引用 next,表本身保存首节点 head,也可以保存尾节点 tail。有效结构满足:从 head 出发沿 next 能依次访问所有节点,既没有重复节点,也没有断开的中间段;末节点的 next 为空。
head → A → B → C (tail) → ∅
若同时维护 head 与 tail,空表时二者都为空;非空时 tail 指向唯一的末节点。这个不变式尤其重要:删掉最后一个节点后,只清空 head 而忘记清空 tail,会留下一个已经不属于当前链表的尾引用。
链表的节点有身份,同值节点也可能是不同对象。查找“值为
直觉
把相邻关系放进节点
数组通过连续位置与下标定位元素;链表把“下一个元素在哪里”直接存进节点。节点可以分散在内存中,只要链接保持正确,遍历就能按顺序到达它们。
因此,链表是一种实现线性序列的存储结构。抽象数据类型规定外部能执行哪些操作,链表规定内部怎样保存元素。栈和队列都可以用链表实现,但它们不同的出入顺序来自接口契约,而不是来自“使用了指针”这个事实。[1]
局部改接为什么能做到常数时间
已知节点 p,要在它后面插入一个尚未加入链表的新节点 x,只需
x.next = p.next
p.next = x
先让新节点接住原来的后继,再让前驱指向新节点。顺序反过来且不保存旧后继,就可能把原链条丢掉,甚至让 x.next 指向自己。若 p 原来是尾节点,还要令 tail = x。[1]
插入前:p ─────────→ q
插入后:p → x → q
删除也要说明手里有什么。若已知待删节点的前驱 p,令 p.next = p.next.next 即可跳过目标;删尾节点还需把 tail 改为 p。若仅拿到单链表中待删节点自身,一般仍需找到前驱,尤其删除尾节点时不能凭这一个引用完成常数时间改接。
“把后继的值复制到当前节点,再删除后继”只能处理部分情况,也改变了被保留节点与值的对应关系;它不适用于尾节点,且在外部持有节点引用时可能违反原本的删除语义。不能用这个技巧无条件证明单链表删除已知节点为
例子与边界
不同操作的成本放在一起看
下表假定引用访问和一次元素操作为
| 操作 | 单链表 | 双向链表 | 动态数组 |
|---|---|---|---|
| 按下标访问第 |
|||
| 无序数据中按值查找 | |||
| 在已知节点后插入 | 中部插入 |
||
| 删除已知节点 | 通常还需寻找前驱 | 中部删除 |
|
| 在尾部加入元素 | 摊还 |
||
| 删除尾部元素 | 摊还 |
双向链表为每个节点增加 prev。删除已知节点时,其前驱和后继都能直接找到,于是只需让两边互相跨过该节点;表头、表尾仍需要相应处理。
这张表解释了为什么“链表适合频繁插删”必须补上条件:操作已经定位到节点,而且查找不是主要成本。若每次都先按下标找到中间位置,再插入一项,整个操作仍然是线性的。
哨兵、遍历与内存布局
哨兵是一个不承载普通元素的辅助节点,可以让“在表头前插入”等边界操作采用统一的链接规则。循环双向链表常用一个哨兵把首尾连起来,但此时停止条件应是“回到哨兵”,不能继续沿用“遇到空指针停止”。结构变化必须和遍历不变式一起说明。
修改链表时,还要保住后续遍历所需的引用。例如删除当前节点后再读取它的 next,在需要显式回收内存的实现中可能已不合法;顺序执行中先保存后继,再断链和回收,才保留了继续遍历的信息。
并发访问还要分开结构更新与生命周期。Harris非阻塞有序链表先在待删节点的next字上标记,再让前驱跨过它,避免与插入竞争时丢掉新节点;Hazard Pointer另行判断哪些已摘除节点仍被读者保护。只保存一个后继引用,并不能保证其他线程不会先释放后继。
链表可以避免搬动未删除的节点,但“节点地址稳定”依赖分配、移动与生命周期策略。每个节点也有额外链接与分配开销;逐指针跳转通常不如连续数组容易利用缓存。具体速度需要结合布局和访问模式比较,不能从
推论与应用
哪些接口能从这些链接获益
在表头插入、删除就能实现栈。保存首尾指针,尾部加入、头部取出就能实现队列。双向链表还适合在已知节点上调整位置,例如哈希表提供定位、链表维护最近使用顺序的 LRU 结构:快速查找来自哈希表,快速重排才来自链表。
不可变单链表可让多个版本共享尾部,从而构造持久数据结构。跳表则额外建立稀疏的前进链接来加速搜索;其期望对数查找来自多层结构与随机化,而不是普通链表本身。把这些机制分开,才能判断一次优化实际消除了哪一项成本。
参考资料
[1] Pat Morin,Open Data Structures: An Introduction,Athabasca University Press,2013,第 3 章 Linked Lists,单链表、双向链表与序列接口。
[2] Robert Sedgewick、Kevin Wayne,Algorithms, 4th ed., Addison-Wesley, 2011,§1.3 Bags, Queues, and Stacks,链式实现、不变式及数组实现的对照。