链表解题:核心思想与 dummy 节点
链表题的核心难点不在逻辑复杂,而在边界条件容易搞错。head 可能为 null,可能被删除,可能只有一个节点。dummy 节点是解决这些问题的钥匙。
核心哲学
“链表题的本质是指针操作,不是数组操作。”
数组你可以随便访问任意位置,链表只能从头一个一个走。所以链表题的核心矛盾是:你只能顺序访问,但题目往往需要你操作特定位置的节点。
数组的随机访问是 O(1),链表是 O(n)。这意味着链表题不能像数组题那样随意用下标访问,所有的”定位”都要靠指针走过去。
dummy 节点(哨兵节点)
dummy 是一个你手动创建的额外节点,放在链表真正的 head 前面。它的值不重要(一般填 0 或 null),它的作用是充当 head 的前驱节点。
dummy → 5 → 6 → 7 → 8 → null
↑ ↑
哨兵 真正的 head
为什么需要它
链表删除操作的本质是:找到被删节点的前一个节点,把它的 next 指针跳过去。
prev → target → next
变成
prev → next (target 被跳过了)
问题来了:如果被删的是 head 呢?
head 没有前驱节点。你没法改”head 的前一个节点的 next 指针”,因为不存在。你必须特殊处理:
// 不用 dummy:删 head 要特殊处理
if (要删的是 head) {
return head.next // 单独写一段逻辑
} else {
prev.next = target.next // 正常删
return head
}用 dummy 之后:
// 用 dummy:删 head 和删其他节点完全一样
dummy.next = head // 先接上
slow = dummy // slow 从 dummy 开始走
// ...走完之后 slow 停在被删节点前一个
slow.next = slow.next.next // 统一操作,不管删的是不是 head
return dummy.next // 返回新 head删 head 时,slow 就是 dummy,dummy.next = head.next 直接跳过 head。删中间节点时,slow 是某个中间节点。代码完全一样,不需要任何特殊分支。
什么时候用 dummy
一句话:当你的操作需要修改节点间的连接关系,且 head 可能被修改或删除时。
| 场景 | 要不要 dummy | 原因 |
|---|---|---|
| 删除节点(head 可能被删) | 要 | head 没有前驱,需要 dummy 充当前驱 |
| 在 head 前面插入新节点 | 要 | 新节点接在 head 前面,dummy 提供”前面” |
| 合并两个有序链表 | 要 | 新链表的 head 不确定是哪个,dummy 提供固定起点 |
| 反转链表 | 不要 | 反转是改 next 方向,不需要前驱 |
| 查找/遍历链表 | 不要 | 只读不改 |
| 判断是否有环 | 不要 | 只读不改 |
直觉判断法
问自己:“代码里会不会出现 if (prev === null) 的特殊分支?”
如果会,就加 dummy。dummy 的作用是把所有”head 的特殊情况”变成”普通情况”,消灭 if-else 分支。
链表题的三个常见坑
坑一:返回值搞混
删除中间节点时,应该返回 head,不是返回被删节点的前一个节点。
[5, 6, 7, 8],删 7
正确:返回 head(值为 5 的节点),输出 [5, 6, 8]
错误:返回 prev(值为 6 的节点),输出 [6, 8]
坑二:边界条件漏判
- head 为 null:直接返回
- 只有一个节点:删它之后返回 null
- k 越界:返回原链表
坑三:用数组存节点
把所有节点存进数组再按下标操作,虽然能 AC,但用了 O(n) 额外空间。面试官会追问”能不能不用数组”。链表题的标准解法应该用指针操作,空间 O(1)。
经典示例
题目:10. One-Pass Removal of k-th Node from End
这道题同时踩了三个坑:返回值搞混(返回了 prev 而不是 head)、边界条件漏判(k 语义理解错)、用数组存节点(不是 one-pass)。正确解法是 dummy + 快慢指针,详见 快慢指针。
总结
遇到链表题,先问自己三个问题:
- head 会被修改或删除吗?会 → 加 dummy。
- 边界条件想全了吗?空链表、单节点、删 head。
- 有没有用数组存节点?有 → 想想能不能用指针替代。
想清楚这三点,再动笔写代码。