LeetCode 19. 删除链表的倒数第 N 个结点
题目描述


题意分析
给定单链表的头节点
head和整数n,删除从链表末尾数起的第n个节点,返回删除后的头节点。倒数第一个节点就是尾节点;若链表长度为L,待删节点就是从头数起的第L - n + 1个节点。题目保证链表非空,且
1 <= n <= L,因此待删节点一定存在。只删除这一个节点,其余节点的先后顺序保持不变。删除头节点后需要返回新的头节点;链表只有一个节点时,删除后返回空。进阶要求是一趟扫描完成,不先遍历一遍计算长度。
解法:快慢指针
核心思路
[!blue]
单链表只能向后访问,不能直接从末尾倒着数。删除一个节点还需要找到它的前驱,才能把前驱的
next改为待删节点的后继。为统一处理删除头节点,在头节点前放置哨兵dummy,让头节点也有前驱。让
fast、slow都从dummy出发,先让fast走n + 1步,再让两个指针每次各走一步。同步移动时,它们之间始终相差n + 1条连接。这里的快慢指针只是出发时间不同,同步阶段的速度相同。把哨兵的位置记为
0,真实节点记为1到L,尾节点之后的空位置记为L + 1。当fast到达空位置时,slow的位置就是(L + 1) - (n + 1) = L - n,恰好是待删节点的前一个位置。这就是间距取n + 1的原因。此时执行slow.next = slow.next.next就能完成删除。若删除头节点,
fast预走后就已为空,slow留在哨兵;若删除尾节点,slow最终停在尾节点的前驱。两种边界都使用相同的接线操作,最后统一返回dummy.next。
解题步骤
- 创建指向
head的哨兵dummy,令fast = slow = dummy。- 让
fast前进n + 1步。题目保证n <= L,因此这段移动最多恰好到达尾后的空位置。- 只要
fast不为空,就同时向后移动fast和slow,保持固定间距。- 循环结束后,
slow.next就是待删节点,将它跳过:slow.next = slow.next.next。- 返回
dummy.next,取得删除后真实的头节点。
代码实现
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
ListNode fast = dummy;
ListNode slow = dummy;
// 从哨兵建立多一步的间距,让慢指针最终停在目标前驱。
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 修改前驱连接,跳过目标节点。
slow.next = slow.next.next;
// 删除原头后入口可能变化,应从哨兵重新取得结果头。
return dummy.next;
}
}
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{Next: head}
fast := dummy
slow := 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
}
复杂度分析
- 时间复杂度:$O(L)$,
L为链表长度。快指针从哨兵走到链表末尾之后,慢指针也只向前移动,没有额外的计数遍历。- 空间复杂度:$O(1)$,只使用一个哨兵和两个指针。
关键点总结
[!green]
- 倒数位置可以通过两个指针的固定间距转化为正向扫描。
- 目标是找到待删节点的前驱;从哨兵出发、以
fast == null结束时,间距应为n + 1。- 哨兵统一处理删除头节点和单节点链表,结果入口始终取
dummy.next。
易错点总结
[!yellow]
- 预走步数必须与起点、终止条件配套。这里若只走
n步,却仍在fast == null时结束,slow会停在待删节点,而非其前驱。- 删除需要改变链表连接。只把
slow移向下一个节点,并不会从链表中删除任何节点。- 返回原
head会在删除头节点时保留错误入口,应返回dummy.next。- 不要额外移动
slow再删除;循环结束时它已经位于正确前驱,slow.next也因n有效而必然存在。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 2095. 删除链表的中间节点 | 中等 | 同样先定位待删节点前驱,原题删中点,本题利用快慢指针的固定间隔定位倒数第n项。 |
| 203. 移除链表元素 | 简单 | 同样用哑节点统一删除头节点,原题按值可能删多个,本题按位置只删一个。 |
| 82. 删除排序链表中的重复元素 II | 中等 | 用哨兵与前驱节点统一链表删改边界;本题先用指针间隔定位倒数节点,该题删除整段重复值。 |
| 83. 删除排序链表中的重复元素 | 简单 | 用哨兵与前驱节点统一链表删改边界;本题先用指针间隔定位倒数节点,该题每段重复值只保留一个。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!