LeetCode 19|中等
核心思想:哑节点 + 快慢指针 + 固定距离
本文目标:不仅会写,还要真正理解“为什么 fast 要先走 n+1 步”
一、题目回顾
给你一个链表 head,删除链表的 倒数第 n 个节点,并返回新的头节点。
限制条件很关键:
- 只能从前往后遍历
- 要求 一次遍历完成
- 单链表,没有前驱指针
二、为什么这道题不“简单”?
如果是数组:
- 倒数第 n 个,直接算下标
但链表的问题在于:
- 你不知道链表长度
- 你不能反向遍历
- 你无法直接定位“倒数”
所以这道题的核心不是删除,而是:
如何在一次遍历中,定位到“待删除节点的前一个节点”
三、整体解题思路
整个解法可以拆成三步:
- 引入哑节点(dummy),统一删除逻辑
- 使用快慢指针,制造一个“固定距离”
- fast 到达末尾时,slow 刚好停在待删除节点的前一个位置
四、完整 Java 实现
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
// fast 先走 n+1 步
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
// fast 和 slow 同时走
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 删除目标节点
slow.next = slow.next.next;
return dummy.next;
}
}
五、为什么一定要引入 dummy 节点?
ListNode dummy = new ListNode(0);
dummy.next = head;
这是一个工程级别的技巧,目的只有一个:
统一删除逻辑,包括删除头节点
例如:
head = [1], n = 1
如果没有 dummy:
- 删除头节点需要特判
- 代码会被 if/else 污染
有了 dummy:
-
删除任何节点都变成一句话
slow.next = slow.next.next;
六、这道题的灵魂:为什么 fast 要先走 n+1 步?
这一点是很多人写对了,但没真正理解的地方。
1. 我们真正想要的是谁?
不是倒数第 n 个节点,而是:
倒数第 n 个节点的前一个节点
因为单链表删除节点,必须知道它的前驱。
2. fast 和 slow 的“距离设计”
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
这一步让:
fast 和 slow 之间,始终保持 n+1 个节点的距离
这个距离在后续过程中是一个不变量。
3. 同步移动时发生了什么?
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
-
fast 向链表末尾冲
-
slow 被“拖着走”
-
当 fast 到达
null时:- slow 刚好站在「待删除节点的前一个位置」
这不是巧合,而是由“固定距离”严格保证的。
七、用具体例子走一遍
链表:
1 → 2 → 3 → 4 → 5
n = 2
目标:删除 4
加入 dummy 后:
dummy → 1 → 2 → 3 → 4 → 5
fast 先走 n+1 = 3 步
fast 在 3
slow 在 dummy
同步移动
| fast | slow |
|---|---|
| 4 | 1 |
| 5 | 2 |
| null | 3 |
此时:
- slow 在
3 - slow.next 正是
4 - 删除动作精准完成
八、时间和空间复杂度
-
时间复杂度:
O(L)- L 为链表长度,只遍历一次
-
空间复杂度:
O(1)- 只使用了常量级指针
九、这道题在双指针体系中的位置
这是一个**“距离型双指针”**的经典模型:
- 不是快慢找中点
- 不是判断是否成环
- 而是:
通过制造相对距离,获得定位能力
同一思想还可以解决:
- 倒数第 k 个节点
- 链表中点
- 删除指定位置节点
十、一句话总结
这道题真正教会你的不是删除节点,而是:
在“只能向前”的链表结构中,用相对距离,换取精准定位。
理解这一点,双指针就不再是模板,而是工具。