LC19
列车检修员如何一趟摘掉倒数第 N 节车厢?
dummy 牵引车 + 蓝色侦察车 + 黄色维修车 = 一趟扫完的摘车术
给一列单向车厢(单链表),删除倒数第 n 节。难点是单链表只能向前走,倒着数行不通。本页用『列车检修』隐喻把dummy / fast / slow 的几何关系画清楚,让陌生人也能看懂为什么 fast 要先走 n+1 条边。
单链表 · 快慢指针一次遍历 · 一趟摘车时间 O(n)空间 O(1)
边界测试 · 切换后整段动画会重算
当前预期输出:1→2→3→5→null
帧 1/14Scene 1/8
当前步骤
Scene 1:列车进场
轨道上是 5 节 车厢:1→2→3→4→5→null。今天的工单:摘掉倒数第 2 节车厢(值 4)。
链表 1→2→3→4→5→null · n=2输入:[1,2,3,4,5],n=2
距离尺 · Gap Meter
期望 n+1 = 3slow
fast
当前距离—(尚未进入快慢阶段)
普通车厢(节点)dummy 牵引车待删故障车厢fast 侦察车slow 维修车null 终点牌(黑洞)
Go 代码 · 同步高亮
模板:fast 先走 n+1 / for fast != nil1func removeNthFromEnd(head *ListNode, n int) *ListNode {2 dummy := &ListNode{Next: head}3 fast, slow := dummy, dummy4 5 for i := 0; i < n+1; i++ {6 fast = fast.Next7 }8 9 for fast != nil {10 fast = fast.Next11 slow = slow.Next12 }13 14 slow.Next = slow.Next.Next15 return dummy.Next16}本页采用 fast 从 dummy 出发先走 n+1 条边,循环条件是 fast != nil。另一种模板是 fast 先走 n 条边, 循环条件通常是 fast.next != nil。两种模板不要混用——混用会差一节车厢,删错位置。
不变量 · Invariants
- I1fast 与 slow 之间的距离恒为 n+1 条边(fast 先走完那 n+1 之后的整段)。
- I2fast == null ⇔ slow 已抵达『倒数第 n+1 节』,亦即待删节点的前驱。
- I3dummy 的存在让『待删 = head』也可表述为 slow.Next,无需特判。
- I4删除是改线,不是销毁:slow.Next = slow.Next.Next,无人引用即被 GC。
易混模板对照 · 切勿混用
模板 A · 本页采用fast 先走 n+1
- · fast 从 dummy 起步
- · 先走 n+1 条边
- · 同步循环条件:fast != nil
- · 结束时 slow 停在 待删前驱
模板 B · 另一种写法fast 先走 n
- · fast 从 dummy 起步
- · 先走 n 条边
- · 同步循环条件:fast.Next != nil
- · 结束时 slow 也停在待删前驱(条件配套不同)
⚠ 把模板 A 的 n+1 与模板 B 的 fast.Next != nil 混着写,会差一节车厢——这是面试最常见的越界/删错节点 bug。
QA 自检 · 边界用例
| 场景 | 输入 | 输出 | slow 停在 |
|---|---|---|---|
| 删除中间节点 | head=[1,2,3,4,5], n=2 | 1→2→3→5→null | 3 号(待删前驱) |
| 删除尾节点 | head=[1,2,3,4,5], n=1 | 1→2→3→4→null | 4 号(待删前驱) |
| 删除头节点 | head=[1,2,3,4,5], n=5 | 2→3→4→5→null | dummy(删头时 slow 不动) |
| 单节点链表 | head=[1], n=1 | [] (空链表) | dummy(删头时 slow 不动) |
前三行可用顶部按钮直接重放动画;第四行(单节点)走的是与『删除头节点』同一条边界路径—— sync 循环 0 次,slow 留在 dummy。