删除倒数第 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)。
这是 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 让「删头」也有前驱,间距不用改。
同步走到 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。不必为头结点写第二份代码。
Go:哨兵 + 快慢指针
func removeNthFromEnd(head *ListNode, n int) *ListNode {dummy := &ListNode{Next: head}fast, slow := head, dummyfor i := 0; i < n; i++ { fast = fast.Next }for fast != nil {fast = fast.Nextslow = slow.Next}slow.Next = slow.Next.Nextreturn 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 步的间距,才把慢指针留在前驱。
- 哨兵让删头与删中间走同一段代码。先数长度再删也对,只是多一趟。