删除链表倒数第 N 个节点为什么用快慢指针?虚拟头节点有什么作用?
简化版
删除倒数第 N 个节点可以用快慢指针一趟完成。
先让快指针领先慢指针 n 步,然后两个指针一起走;当快指针到末尾时,慢指针就在待删除节点的前一个位置。
为了统一删除头节点的情况,通常加一个虚拟头节点 dummy。
详细版
这题的核心是把“倒数第 N 个”转成“两个指针之间保持 N 个节点间隔”。
创建 dummy 指向 head,让 fast 和 slow 都从 dummy 出发。先让 fast 走 n 步,再让 fast 和 slow 同步前进,直到 fast.next === null。
此时 slow.next 就是要删除的节点,执行:
slow.next = slow.next.next
最后返回 dummy.next。
这样即使删除的是原头节点,也不需要单独写分支。时间复杂度 O(n),空间复杂度 O(1)。
完整版教学
一、为什么需要快慢指针
链表不能像数组一样通过下标直接访问倒数第 N 个节点。最直观的方法是先遍历一遍求长度,再计算正数位置,再遍历第二遍删除。快慢指针的价值在于用“距离差”代替“长度计算”,从而一趟扫描完成。快指针先走 n 步后,慢指针和快指针之间就保持了固定间隔。
记忆钩子:倒数问题不要急着数长度,先想“让一个指针领先 N 步”。
二、间隔为什么刚好能定位前驱节点
假设链表长度是 5,要删除倒数第 2 个节点,也就是第 4 个节点。
dummy -> 1 -> 2 -> 3 -> 4 -> 5
slow delete
fast 到尾部
如果 fast 先领先 2 步,然后一起走到 fast.next == null,慢指针会停在删除节点的前一个节点。删除链表节点时必须拿到前驱节点,因为单向链表无法从当前节点回到前一个节点。
三、为什么要用 dummy
如果要删除的是头节点,例如链表 [1,2,3] 删除倒数第 3 个,真实答案是 [2,3]。没有 dummy 时,头节点没有前驱,代码需要单独处理 head = head.next。加上 dummy 后,头节点也有了统一的前驱节点。
| 情况 | 没有 dummy | 有 dummy |
|---|---|---|
| 删除中间节点 | 正常改前驱指针 | 正常改前驱指针 |
| 删除尾节点 | 正常改前驱指针 | 正常改前驱指针 |
| 删除头节点 | 需要特判 | 统一处理 |
dummy 的作用不是改变链表含义,而是让边界情况变普通。
四、代码模板
标准写法如下:
function removeNthFromEnd(head, n) {
const dummy = { next: head }
let fast = dummy
let slow = dummy
for (let i = 0; i < n; i++) {
fast = fast.next
}
while (fast.next !== null) {
fast = fast.next
slow = slow.next
}
slow.next = slow.next.next
return dummy.next
}
注意循环条件是 fast.next !== null,因为我们希望 slow 停在待删除节点的前驱,而不是待删除节点本身。
五、边界例子怎么手算
链表 [1] 删除倒数第 1 个。dummy -> 1,fast 先走一步到 1,此时 fast.next == null,slow 仍在 dummy。执行 slow.next = slow.next.next 后,dummy 指向 null,返回空链表。
链表 [1,2] 删除倒数第 1 个。fast 先到 1,同步走到 2,slow 到 1,删除 slow.next 正好删除尾节点。
这些小例子能验证模板没有偏一位。
六、常见误区与追问
- 误区:让 fast 先走 n + 1 步但循环条件不改。 领先步数和停止条件必须配套,否则慢指针会偏一位。
- 误区:不用 dummy 然后忘记删除头节点。 倒数第 N 个可能就是第一个节点,必须覆盖这个边界。
- 误区:让 slow 停在待删除节点。 单向链表删除需要前驱节点,停在当前节点反而不好改链接。
- 追问:能不能两趟遍历? 可以,先求长度再删除,复杂度仍是
O(n),但一趟快慢指针更简洁。 - 追问:如果 n 非法怎么办? LeetCode 类题通常保证合法;工程代码应检查
n > 0且不超过长度。
这些追问的重点是链表删除依赖前驱节点,而不是只找到目标节点。
七、加强记忆
这题记成“dummy 保头,fast 领先,slow 找前驱”。dummy 解决删除头节点;fast 先走 n 步制造倒数距离;同步移动时保持这个距离;fast 到最后一个节点时,slow 正好在待删除节点前面。只要领先步数和停止条件配套,整题就不会乱。