力扣刷题#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=1return head->next = return nullptr[1,2] n=2return 2

阶段二(第 12-16 行):

while (fast->next) {
    slow = slow->next;
    fast = fast->next;
}

关键:条件是 fast->next(不是 fast)。这样 fast 停在最后一个节点时退出——slow 刚好在待删节点的前一个

删除操作(第 19 行):

slow->next = slow->next->next;

经典的链表删除:让 slow 跳过下一个节点,直接连到下下个。无论下一个节点的 nextnullptr(删尾)还是非空(删中间),这行都正确。


优化分析

优化项 说明
去掉 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仅负责文本修饰,综合获得以上内容。

posted @ 2026-06-23 23:20  STA_running  阅读(2)  评论(0)    收藏  举报