【LeetCode Hot100 刷题日记(29/100)】19.删除链表的倒数第 N 个结点——链表、双指针、栈(双指针法详解)📌

7 阅读6分钟

📌 题目链接:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:链表、双指针、栈

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)(最优解)


核心算法:双指针(快慢指针)
🎯 面试常考:链表操作、边界处理、一次遍历优化
💡 进阶考点:如何避免重复遍历?如何处理头节点删除?如何用哑节点统一逻辑?


📌 题目分析

我们被要求删除一个单向链表中倒数第 n 个节点,并返回新的链表头。

🔎 关键挑战:

  • 无法直接访问倒数节点:链表是单向的,只能从头到尾遍历。
  • 删除操作需前驱节点支持:要删除某个节点,必须知道它的前一个节点。
  • 边界情况复杂:比如 n == 链表长度 时,需要删除的是头节点;n == 1 时,删除尾节点。

🧩 示例回顾:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

👉 倒数第 2 个是 4,删除后变为 [1,2,3,5]


🧠 核心算法及代码讲解

✅ 推荐方法:双指针法(快慢指针)

这是本题的最优解法,满足“一趟扫描完成”的进阶要求。

🔍 算法思想:

使用两个指针 firstsecond,让 first 先走 n 步,然后两者同时前进。当 first 到达末尾时,second 恰好指向待删除节点的前驱节点

这样就可以直接进行删除操作:second->next = second->next->next;

✅ 为什么可行?

  • first 超前 n 步 → 两指针之间相隔 n 个节点。
  • first 走完链表,second 自然落在倒数第 n 个节点的前面。
  • 无需计算链表长度,也无需额外空间。

🛠️ 技巧:引入哑节点(dummy node)

  • 解决删除头节点的特殊情况。
  • 统一所有情况的处理逻辑,避免写 if-else 分支。

💡 哑节点是链表题中的“神器”,在面试中经常出现!


🧩 解题思路(分步解析)

  1. 创建哑节点

    ListNode* dummy = new ListNode(0, head);
    
    • dummy->next = head
    • 无论删除哪个节点,都能通过 dummy 安全地获取前驱。
  2. 初始化双指针

    • first = head(快指针)
    • second = dummy(慢指针)
  3. 让 first 先走 n 步

    • 循环 n 次,first = first->next
    • 此时 firstsecond 超前 n 个位置。
  4. 双指针同步移动

    • 同时移动 firstsecond,直到 first == nullptr
    • 此时 second 指向的是待删节点的前驱
  5. 执行删除操作

    second->next = second->next->next;
    
  6. 返回结果

    return dummy->next;
    
  7. 释放内存(可选)

    • 实际面试中需与面试官确认是否释放被删节点内存。

📊 算法分析

方法时间复杂度空间复杂度是否一次遍历是否推荐
计算长度 + 遍历O(L)O(1)❌ 两次遍历⭐⭐
O(L)O(L)✅ 一次遍历
双指针(推荐)O(L)O(1)✅ 一次遍历⭐⭐⭐⭐⭐

最优选择:双指针法 —— 时间和空间都最优,且逻辑清晰,适合面试!


💻 代码实现

#include 
using namespace std;
using ll = long long;

// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 🛠️ 创建哑节点,简化头节点删除的边界处理
        ListNode* dummy = new ListNode(0, head);
        
        // 🚀 快指针 first,先走 n 步
        ListNode* first = head;
        for (int i = 0; i < n; ++i) {
            first = first->next;  // 👉 first 先走 n 步
        }
        
        // 🔄 慢指针 second 从 dummy 开始,与 first 同步移动
        ListNode* second = dummy;
        while (first != nullptr) {
            first = first->next;  // 👉 first 走到末尾
            second = second->next; // 👉 second 走到倒数第 n 个节点的前驱
        }
        
        // 🔥 执行删除:跳过目标节点
        second->next = second->next->next;
        
        // 📥 返回新链表头(注意不是 dummy!)
        ListNode* ans = dummy->next;
        
        // 🧹 释放哑节点(实际面试中可省略,视需求而定)
        delete dummy;
        
        return ans;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 🧪 测试用例 1: [1,2,3,4,5], n=2 -> [1,2,3,5]
    ListNode* head1 = new ListNode(1);
    head1->next = new ListNode(2);
    head1->next->next = new ListNode(3);
    head1->next->next->next = new ListNode(4);
    head1->next->next->next->next = new ListNode(5);

    Solution sol;
    ListNode* result1 = sol.removeNthFromEnd(head1, 2);

    // 输出结果
    while (result1) {
        cout << result1->val << &#34; &#34;;
        result1 = result1->next;
    }
    cout << endl;

    // 🧪 测试用例 2: [1], n=1 -> []
    ListNode* head2 = new ListNode(1);
    ListNode* result2 = sol.removeNthFromEnd(head2, 1);
    if (result2 == nullptr) cout << &#34;[]\n&#34;;
    else {
        while (result2) {
            cout << result2->val << &#34; &#34;;
            result2 = result2->next;
        }
        cout << endl;
    }

    // 🧪 测试用例 3: [1,2], n=1 -> [1]
    ListNode* head3 = new ListNode(1);
    head3->next = new ListNode(2);
    ListNode* result3 = sol.removeNthFromEnd(head3, 1);
    while (result3) {
        cout << result3->val << &#34; &#34;;
        result3 = result3->next;
    }
    cout << endl;

    return 0;
}

🧠 面试拓展知识点

🔹 1. 为什么不用 stack

虽然栈能解决问题,但空间复杂度为 O(L),不符合“常数空间”的期望。

🚫 面试中若使用栈,会被问:“有没有更优的空间方案?” → 应立刻想到双指针。

🔹 2. 如何处理内存释放?

  • C++ 中手动管理内存,需 delete 被删除节点(但本题未释放)。
  • Java/Python 自动 GC,无需关心。
  • 面试建议:提前沟通是否需要释放内存。

🔹 3. 边界情况验证

情况是否覆盖
删除头节点✅(哑节点解决)
删除尾节点
单节点链表
n == 链表长度

✅ 本解法全部覆盖!

🔹 4. 双指针的通用模板

ListNode* first = head;
ListNode* second = head;
for (int i = 0; i < k; ++i) first = first->next;
while (first) {
    first = first->next;
    second = second->next;
}
// second 现在指向距离末尾 k 个节点的位置

💡 这种模式可用于“找倒数第 k 个节点”、“链表相交点”等问题。


🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第30题 —— 24.两两交换链表中的节点(中等)

🔹 题目:给定一个链表,两两交换相邻的节点,并返回交换后的链表。

🔹 示例[1,2,3,4][2,1,4,3]

🔹 核心思路:递归或迭代方式,每次交换两个节点,注意维护前后连接关系。

🔹 考点:链表操作、指针重连、递归思维、边界处理(奇偶长度)。

🔹 难度:中等,但非常经典,常出现在大厂面试中!

💡 提示:可以尝试用递归迭代两种方式实现,对比它们的优劣!

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!