链表解题:核心思想与 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 + 快慢指针,详见 快慢指针


总结

遇到链表题,先问自己三个问题:

  1. head 会被修改或删除吗?会 → 加 dummy。
  2. 边界条件想全了吗?空链表、单节点、删 head。
  3. 有没有用数组存节点?有 → 想想能不能用指针替代。

想清楚这三点,再动笔写代码。