D

当前:LC19 · 删除链表的倒数第 N 个结点 · 首次出现于 Day 10 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

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 = 3
slow
fast
当前距离—(尚未进入快慢阶段)
回收区 · GC drop zone1#02#13#24#3⚠ 故障 · 待删5#4null终点牌
普通车厢(节点)dummy 牵引车待删故障车厢fast 侦察车slow 维修车null 终点牌(黑洞)

Go 代码 · 同步高亮

模板:fast 先走 n+1 / for fast != nil
1func removeNthFromEnd(head *ListNode, n int) *ListNode {
2 dummy := &ListNode{Next: head}
3 fast, slow := dummy, dummy
4 
5 for i := 0; i < n+1; i++ {
6 fast = fast.Next
7 }
8 
9 for fast != nil {
10 fast = fast.Next
11 slow = slow.Next
12 }
13 
14 slow.Next = slow.Next.Next
15 return dummy.Next
16}
本页采用 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=21→2→3→5→null3 号(待删前驱)
删除尾节点head=[1,2,3,4,5], n=11→2→3→4→null4 号(待删前驱)
删除头节点head=[1,2,3,4,5], n=52→3→4→5→nulldummy(删头时 slow 不动)
单节点链表head=[1], n=1[] (空链表)dummy(删头时 slow 不动)

前三行可用顶部按钮直接重放动画;第四行(单节点)走的是与『删除头节点』同一条边界路径—— sync 循环 0 次,slow 留在 dummy。

步骤时间线 · 14