Pattern 06 · Linked List

06 链表 只有指针改写和快慢指针两件事;画图 > 心算

面试频率 高题目 9 道模板 2 个⭐认脸 1 道
§1 Mental Model 一句话心智模型

先记住这一句

链表题只有两件事:改指针,和跑指针。改指针之前一定先把 next 存下来;跑指针靠一快一慢制造出位置差。

改指针类(反转、合并、删除、重排)的通用保险是哨兵节点 dummy:它让「头节点也可能被删 / 被换」这个特判彻底消失,代码短一半、bug 少一半。

跑指针类(找中点、判环、倒数第 n 个)本质是第 02 章的快慢指针,只是数据结构换成了链表。画图,不要心算——三个节点、两轮循环,纸上画一遍比在脑子里推十遍准。

§2 Recognition Signals 识别信号

看到什么,想到本章

题面出现…想到
「反转 / 翻转链表(或其中一段)」三指针改写(模板 A)
「合并两个有序链表」「删除倒数第 n 个」哨兵 + 双指针(模板 B)
「找中点 / 判断有环 / 环的入口」快慢指针,快的每次两步
「重排 / 判回文链表」找中点 → 反转后半 → 交替拼接,三个已知动作的组合
「带随机指针的深拷贝」哈希表:旧节点 → 新节点(第 01 章的映射思维)
「O(1) 时间的 get 和 put」哈希表 + 双向链表(146 LRU)
分流条件 · 决策树同源

快慢指针与第 02 章同源,区别只是数组能随机访问、链表只能顺着走。「合并 K 个有序链表」的主语已经不是链表而是堆(第 08 章)——两个链表用双指针,K 个链表要的是「当前最小的头节点」这种 Top-K 结构。「链表排序」考的是归并思路,不在阈值范围内。

§3 Template 模板代码

可默写的那几行

■ 【骨架】 不变,闭着眼默写 ■ 【插槽】 随题变化,写题前先问自己填什么

模板 A · 反转改指针类的地基
def reverse_list(head):
    prev, cur = None, head                  # 【骨架】prev 是已反转部分的头
    while cur:                              # 【骨架】
        nxt = cur.next                      # 【骨架】先记住 next —— 全章第一守则
        cur.next = prev                     # 【插槽】改指针:反转 / 断开 / 指向别处
        prev, cur = cur, nxt                # 【骨架】两个指针一起前进
    return prev                             # 【插槽】答案:新头 / 新尾 / 一段的两端
默写口诀:存 next、改指向、双双前进。反转一段(92、25)时骨架不变,只是循环跑固定次数、并在两端重新接线。
模板 B · 哨兵 + 双指针合并 / 删除类
def merge_two(a, b):
    dummy = tail = ListNode()               # 【骨架】哨兵头,省掉"第一个节点"的特判
    while a and b:                          # 【骨架】
        if a.val <= b.val:                  # 【插槽】选谁接上来的条件
            tail.next, a = a, a.next        # 【骨架】接上 + 被选中的一方前进
        else:
            tail.next, b = b, b.next        # 【骨架】
        tail = tail.next                    # 【骨架】
    tail.next = a or b                      # 【插槽】收尾:剩下的一整段直接挂上
    return dummy.next                       # 【骨架】哨兵的下一个才是真头
只要题目里「头节点可能被改动」,第一行就写 dummy。最后 return dummy.next,不是 return head。
§4 Problems 题目分层

从建直觉到扭模板

热身建直觉,easy 为主1 道
  • 模板 A 原题。递归写法也要会说:reverse(head.next) 之后head.next.next = head; head.next = None,栈深度 O(n) 是它的代价。
标准模板直接套5 道
  • 模板 B 原题。收尾那行 tail.next = a or b 是链表相对数组的便宜:剩下的一整段不用逐个搬。
  • 快慢指针判环。为什么快指针走两步就一定能追上:每轮差距缩小 1,环内必然相遇。
  • 141 的进阶:相遇后从头和从相遇点同速前进,交点即入口。结论要背,推导(设环前长 a、环长 b)能讲一遍更好。
  • 哨兵 + 快指针先走 n + 1 步。为什么是 n + 1 而不是 n:要让 slow 停在待删节点的前一个。
  • 三个已知动作的拼装:找中点(快慢)→ 反转后半(模板 A)→ 交替合并。本章最能体现「链表题是动作的组合」的一道。
变体模板要扭,坑的来源3 道
  • 哈希映射旧 → 新,两遍扫描:第一遍建节点,第二遍接 next 和 random。O(1) 空间的原地交织解法是加分项,不是必需品。
  • 哈希表 + 双向链表。面试里可以提 OrderedDict 一行搞定,但必须能手写双向链表版本——考的就是这个。
  • ⭐认脸。K 路归并:堆里放 K 个头节点,见第 08 章。
§5 Pitfalls & Self-check 常见翻车点 + 自测

会写不等于不翻车

  1. 先改指针,再想起要 next——cur.next = prev 之后原来的 cur.next 就永远找不回来了。任何改指针的循环,第一行都应该是 nxt = cur.next。
  2. 不用哨兵——头节点可能被删(19)、可能被换(21、203)。没有 dummy 就得为「删的是头」单独写一段,这段代码是链表题最常见的漏写。
  3. 快指针的循环条件——while fast and fast.next 缺一不可:只判 fast 会在 fast.next.next 处崩,只判 fast.next 会在链表为空时崩。
  4. 环入口结论记成「相遇点就是入口」——相遇点在环内但通常不是入口。必须再走一轮同速追赶(见 142)。
  5. 双向链表忘了断两头——146 里删除节点要同时修 prev.next 和 next.prev,只改一边会留下悬挂引用,表现为容量统计对不上。

自测 · 30 秒识别模式

30 秒识别Q1 · 83 Remove Duplicates from Sorted List:删除已排序链表中所有重复的节点,每个值只保留一个。你的第一反应是哪个模式?

看答案

链表。改指针类,cur.next = cur.next.next 跳过重复即可,连哨兵都不需要(头节点一定保留)。用 set 去重也能过,但那是数组思维,浪费了「已排序」。

30 秒识别Q2 · 287 Find the Duplicate Number:长度 n+1 的数组,元素都在 1..n 之间,找出那个重复的数(不能改数组、只能用 O(1) 空间)。你的第一反应是哪个模式?

看答案

链表的快慢指针。把 i -> nums[i] 看成链表的 next,重复的数就是环的入口,142 原样套用。这是本章最值得认脸的一道——它长得完全不像链表题。(允许用哈希时当然是第 01 章。)

30 秒识别Q3 · 155 Min Stack:设计一个栈,push / pop / top / getMin 全部 O(1)。你的第一反应是哪个模式?

看答案

栈,不是本章——同样是「设计一个数据结构」,146 LRU 要的是「按访问顺序淘汰」所以需要双向链表,而这道要的是「随时取当前最小」,辅助栈同步记录即可。设计题不都是链表题,看它要维护的到底是什么顺序。

每章自测都会混入恰好一道非本章题,防止「读哪章选哪章」的条件反射;题面也一律取自本章未收录的题,避免凭记忆而非凭信号作答。