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

LC19 · Remove Nth Node From End · 链表

删除倒数第 N 个结点:让慢指针慢 N 拍

删除要前驱。从 dummy 到 nil 比从待删前驱到 nil 多 n+1 步,快的先走完这段间距。

在 head 前接哨兵 dummy。待删节点的前驱走到 nil 需要 n+1 步。让 fast 从 dummy 先走 n+1 步(代码里等价于 slow 停在 dummy、fast 从 head 先走 n 步),再与 slow 同步前进,直到 fast 变成 nil。此时 slow 就是前驱,执行 slow.Next = slow.Next.Next。删头结点也走同一条路。时间 O(n),空间 O(1)。

时间 O(n)空间 O(1)结论先行 · 全文约 6 节
导读

这是 LeetCode 19. Remove Nth Node From End of List。给你单链表头节点 head 和整数 n,删掉倒数第 n 个节点,返回新的头节点。n 保证合法,不会超出链表长度。

主例 1→2→3→4→5,n=2。倒数第 2 个是 4,删掉后 1→2→3→5。对照 n=5:删的是头结点 1,返回 2→3→4→5。单节点 1、n=1,删完返回空。

先走一遍数长度 L,再删正数第 L−n+1 个,两趟扫描能做对。缺的是第二趟:单链表没有回退,倒数位置要在第一趟结束时就定位到前驱。本文用手走主例的间距和同步,看慢指针为什么停在 3 而不是 4。

倒数能换成正数,但前驱更值钱

倒数第 n 个就是正数第 L−n+1 个。先数 L 再走一遍,正确,却把同一条链走了两次。删节点改的是前驱的 Next,停在待删节点本身还得再找前一个,头结点没有前驱,边界另写。

缺口是:不先知道 L,也要让一个指针停在前驱。在 head 前接 dummy。从待删前驱出发,走 n 步到链尾最后一个节点,再走 1 步到 nil,一共 n+1 步。谁先把这 n+1 步走完,谁就比慢指针整整超前「前驱到 nil」这段路。

于是 fast 从 dummy 先走 n+1 步。主例 n=2,三步是 dummy→1→2→3,fast 停在 3。代码写成 slow 留在 dummy、fast 从 head 走 n 步:head 相对 dummy 已经迈出第 1 步,再走 2 步同样落到 3。演示第一帧:fast 在 3,slow 还在 dummy。

为什么是 n+1 不是 n从待删节点走到 nil 是 n 步,停在待删节点上删不掉它。从它的前驱走到 nil 才是 n+1 步。dummy 让「删头」也有前驱,间距不用改。
fast 领跑 n=2 步,二者保持间隔
1
2
3fast
4
5
fast 先走 2 步(到 3)

同步走到 nil,慢指针就是前驱

间距拉开之后,fast 与 slow 每次各走一步,直到 fast 变成 nil。两人始终隔着 n+1 步。fast 走出链表的那一刻,slow 刚好走完「dummy 到前驱」这段。

主例接着走。fast 在 3、slow 在 dummy:一步之后 fast=4、slow=1;再一步 fast=5、slow=2;再一步 fast=nil、slow=3。slow 停在 4 的前驱。演示把「fast 到尾」画在结点 5 上、slow 画在 3 上,随后 3.Next 改指向 5,4 被跳过,链变成 1→2→3→5。

n 等于链表长度时,fast 从 head 走 n 步会先变成 nil,同步循环一次都不跑,slow 留在 dummy,dummy.Next = dummy.Next.Next 正好删掉头结点,返回 dummy.Next。不必为头结点写第二份代码。

fast 到尾 → slow 停在前驱
1
2
3slow
4
5fast
fast 到尾(5),slow 停在 3(4 的前驱)

Go:哨兵 + 快慢指针

solution.goGo
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{Next: head}
fast, slow := head, dummy
for i := 0; i < n; i++ { fast = fast.Next }
for fast != nil {
fast = fast.Next
slow = slow.Next
}
slow.Next = slow.Next.Next
return dummy.Next
}

1dummy 接在 head 前。删头时 slow 停在 dummy,返回值仍是 dummy.Next。

2slow 从 dummy 起,fast 从 head 起。head 相对 dummy 已多走 1 步。

3fast 再走 n 步,总间距就是 n+1。主例走到 3。

4同步直到 fast 变 nil。主例 slow 落到 3,也就是 4 的前驱。

5前驱的 Next 跳过待删节点。返回哨兵后面那条链。

总结

快的先拉开 n+1,再一起走。主例慢指针停在 3,跳过 4,留下 1→2→3→5。

  • 删除改的是前驱的 Next。停在待删节点上,单链表退不回去。
  • 从 dummy 走 n 步只到待删节点;再多 1 步的间距,才把慢指针留在前驱。
  • 哨兵让删头与删中间走同一段代码。先数长度再删也对,只是多一趟。
同族题目
LC206反转链表(同款双指针)LC141环形链表(快慢指针)LC876链表的中间结点