力扣刷题#19-0019-删除链表的倒数第N个结点
力扣刷题#19-0019-删除链表的倒数第 N 个结点
题目
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:尝试使用一趟扫描实现。
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
解释:倒数第 2 个是 4,删掉后得 [1,2,3,5]
输入:head = [1], n = 1
输出:[]
输入:head = [1,2], n = 1
输出:[1]
我的第一版思路:指针数组
看到"倒数第 n 个",我第一反应是——链表没有下标,但不妨碍我自己造一个"下标"。
方案:遍历一遍把每个 ListNode* 塞进 vector,然后用 vec[sz - n] 直接定位目标。
// 思路:空间换时间
vector<ListNode*> vec;
ListNode* p = head;
while (p) {
vec.push_back(p);
p = p->next;
}
int idx = vec.size() - n;
if (idx == 0) return head->next; // 删头
vec[idx-1]->next = vec[idx]->next; // 删中间或尾部
return head;
| 维度 | 指针数组法 |
|---|---|
| 时间复杂度 | O(n) |
| 空间复杂度 | O(n) |
| 优点 | 思路直白,不容易写错 |
| 缺点 | 需要额外的 vector 空间 |
这个方案能 AC,逻辑也很清晰。但题目问"一趟扫描"(暗示不用额外空间),所以我改用了双指针。
第二版:快慢双指针(我的提交版本)
核心思路
快指针先走 n 步,拉开距离。然后快慢指针一起走——快指针到头时,慢指针指向的下一个节点就是要删的。
生活化类比:
两个人站一排,A 先往前走 n 步。然后两人以相同速度往前走。A 撞到终点墙时,B 离终点刚好是 n 步——B 身后 n 步就是 A 起始的位置,B 前面的那个节点就是要删的倒数第 n 个。
分两种情况
情况一:删的不是头节点(n < 链表长度)
链表: 1 → 2 → 3 → 4 → 5 → nullptr
n=2(删倒数第2个 = 节点4)
第1步: fast 先走 n=2 步
fast = 节点3
第2步: slow 和 fast 一起走,直到 fast 到最后一个
slow fast
节点1 节点3
节点2 节点4
节点3 节点5 ← fast->next == nullptr,停!
第3步: slow 指向节点3,slow->next 是节点4 = 要删的
slow->next = slow->next->next
节点3 直接连到 节点5 ✓
情况二:删头节点(n = 链表长度)
链表: 1 → 2 → 3 → nullptr
n=3(删倒数第3个 = 头节点)
fast 走 3 步: 1→2→3→nullptr ← fast 掉出链表了!
此时直接 return head->next ✓
我的 AC 代码
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* slow = head, *fast = head;
// 阶段一:fast 先走 n 步
while (n--) {
fast = fast->next;
}
// 若 fast 走穿 → n 等于链表长度 → 删的是头节点
if (fast == nullptr) return head->next;
// 阶段二:fast 和 slow 一起走,fast 到最后一个节点时停
while (fast->next) {
slow = slow->next;
fast = fast->next;
}
// slow 停在待删节点的前一个,执行删除
slow->next = slow->next->next;
return head;
}
};
代码逐步解析
阶段一(第 5-7 行):
while (n--) {
fast = fast->next;
}
让 fast 先走 n 步,拉开与 slow 的"安全距离"。这个距离恰好是 n 个节点。
判空(第 10 行):
if (fast == nullptr) return head->next;
如果 fast 走完后已经是 nullptr,说明 n 不小于链表长度。链表中要删的节点恰好在头或头之前——直接返回 head->next。
为什么是
return head->next而不是return nullptr?因为题目保证 n 有效(1 ≤ n ≤ 链表长度),所以 n 不可能超过链表长度。
fast==nullptr时 n 恰恰等于链表长度,要删的就是 head。比如[1] n=1→return head->next=return nullptr;[1,2] n=2→return 2。
阶段二(第 12-16 行):
while (fast->next) {
slow = slow->next;
fast = fast->next;
}
关键:条件是 fast->next(不是 fast)。这样 fast 停在最后一个节点时退出——slow 刚好在待删节点的前一个。
删除操作(第 19 行):
slow->next = slow->next->next;
经典的链表删除:让 slow 跳过下一个节点,直接连到下下个。无论下一个节点的 next 是 nullptr(删尾)还是非空(删中间),这行都正确。
优化分析
| 优化项 | 说明 |
|---|---|
去掉 if(n==1 && head->next==nullptr) |
已被 if(fast==nullptr) 覆盖(n=1 且只有一个节点时,fast 走 1 步 = nullptr) |
去掉未使用的 tmp 变量 |
删掉即可 |
| 加 dummy 节点 | 另一种风格:ListNode dummy(0, head),可以统一处理删头和非删头,不用单写 if(fast==nullptr) |
思路对比
| 方案 | 时间 | 空间 | 遍历次数 | 适用场景 |
|---|---|---|---|---|
| 指针数组(初版) | O(n) | O(n) | 1 遍 | 逻辑直白,不怕写错 |
| 快慢双指针(提交版) | O(n) | O(1) | 1 遍 | 空间最优,面试必会 |
| dummy + 双指针 | O(n) | O(1) | 1 遍 | 代码更统一,无需头节点特判 |
复杂度分析
| 维度 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 两个指针各遍历一次,总计线性 |
| 空间复杂度 | O(1) | 只用了 slow、fast 两个指针 |
本文档由 AI 辅助生成,作者提供问题,思路和代码,AI仅负责文本修饰,综合获得以上内容。

浙公网安备 33010602011771号