Leetcode 106 删除链表的倒数第 N 个结点

1 题目

19. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n个结点,并且返回链表的头结点。

示例 1:

复制代码
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

复制代码
输入:head = [1], n = 1
输出:[]

示例 3:

复制代码
输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

**进阶:**你能尝试使用一趟扫描实现吗?

2 代码实现

cpp

cpp 复制代码
/**
 * 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);
        dummy -> next = head;

        ListNode* fast = dummy;
        ListNode* slow = dummy;

        for (int i = 0 ; i < n ; i++){
            fast = fast -> next;
        }
        while(fast -> next != nullptr){
            fast = fast -> next ;
            slow = slow -> next ;
        }
        ListNode* temp = slow -> next ;
        slow -> next = slow -> next -> next ;
        delete temp;

        ListNode* new_head = dummy -> next ;
        delete dummy ;
        return new_head;
    }
};

js

javascript 复制代码
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
var removeNthFromEnd = function(head, n) {
    const dummy = new ListNode(0);
    dummy.next = head ;

    let fast = dummy;
    let slow = dummy;

    for (let i = 0; i < n ; ++i){
        fast = fast.next ;
    }

    while(fast.next ){
        slow =slow.next ;
        fast = fast.next ;
    }
    slow.next = slow.next.next ;

    const new_head = dummy.next ;
    return new_head;
};

(js是自己写的哦,js好简单!)

思考

虽然这个题目是很早之前做过的,但是我现在回头看看也不是很会,只要还是这个倒数。

一次遍历得到长度,第二次遍历定位要删除的节点位置 (长度 - 倒数)。

呃我偷看了提示,我知道了,两个指针同时出发,间隔是倒数的长度,打头的那个指针走到空了,说明后面那个指针已经到倒数的位置了,我写一些测试的数据看一下条件,没什么问题,思路蛮简单的,代码具体怎么写再看看细节。

自己写的错误百出的代码

cpp 复制代码
/**
 * 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* fast = head ;
        if(fast!= nullptr){
            for (int i = 0 ; i < n ; i++){
                fast = fast -> next ;
            }
        }else{
            return head;
        }

        ListNode*slow = head ;
        while(fast -> next != nullptr){
            slow = slow -> next ;
            fast = fast -> next ;
        }
        ListNode* temp = slow -> next ;
        slow -> next = slow -> next -> next ;
        delete temp;
        return head ;
    }
};

我自己反思了一下,头结点有没有可能被删?啊,当然有可能啊,怎么考虑,不知道怎么写了。应该就是虚拟头结点啊。

你现在想解决的是删除链表倒数第 N 个结点的问题,并且希望用一趟扫描实现,核心困扰是不知道如何处理头结点被删除的边界情况,你写的代码也因为没考虑这个问题导致错误。

正确的解题思路(双指针 + 虚拟头结点)

  1. 虚拟头结点(dummy):解决删除头结点时的边界问题,让所有结点的删除逻辑统一。
  2. 快慢指针
    • 快指针先向前走 n 步。
    • 快慢指针再一起向前走,直到快指针走到链表末尾(fast == nullptr)。
    • 此时慢指针的下一个结点就是要删除的倒数第 N 个结点。

完整可运行的代码

cpp 复制代码
/**
 * 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) {
        // 1. 创建虚拟头结点,指向原头结点(解决删除头结点的边界问题)
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        
        // 2. 初始化快慢指针,都从虚拟头结点出发
        ListNode* fast = dummy;
        ListNode* slow = dummy;
        
        // 3. 快指针先向前走 n 步
        for (int i = 0; i < n; i++) {
            fast = fast->next;
        }
        
        // 4. 快慢指针一起走,直到快指针走到链表末尾
        while (fast->next != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }
        
        // 5. 删除慢指针的下一个结点(倒数第 N 个结点)
        ListNode* temp = slow->next;  // 保存要删除的结点,用于释放内存
        slow->next = slow->next->next;
        delete temp;  // 释放内存,避免内存泄漏
        
        // 6. 返回新的头结点(虚拟头结点的下一个)
        ListNode* newHead = dummy->next;
        delete dummy;  // 释放虚拟头结点的内存
        return newHead;
    }
};

代码关键部分解释

  1. 虚拟头结点 dummy
    • 比如输入是 [1]n=1 时,原头结点 1 要被删除,若没有 dummy,直接操作 head 会无法处理;有了 dummy 后,slow 会停在 dummy 位置,删除 dummy->next 即可。
    • 所有结点的删除逻辑都统一为 "删除慢指针的下一个结点",无需单独判断是否删头结点。
  2. 快慢指针的移动
    • 快指针先移 n 步,保证快慢指针的间隔是 n
    • 一起移动到快指针末尾时,慢指针的下一个就是目标结点(比如示例 1 中,快指针移 2 步到 3,然后一起移到快指针到 5 的下一个(nullptr),慢指针停在 3,下一个 4 就是要删的)。
  3. 内存释放delete tempdelete dummy 是为了避免内存泄漏,符合 C++ 的最佳实践。

测试用例验证

  • 示例 1:输入 head = [1,2,3,4,5], n=2 → 输出 [1,2,3,5]
  • 示例 2:输入 head = [1], n=1 → 输出 []
  • 示例 3:输入 head = [1,2], n=1 → 输出 [1]

总结

  1. 核心技巧 :用虚拟头结点 解决删除头结点的边界问题,用双指针(快慢指针) 实现一趟扫描找到倒数第 N 个结点。
  2. 关键逻辑 :快指针先移 n 步,快慢指针再同步移动,直到快指针到末尾,此时慢指针的下一个就是要删除的结点。
  3. 内存管理 :C++ 中手动创建的结点要记得 delete,避免内存泄漏。

你的原代码出错的核心原因是:没有处理头结点被删除的情况(比如示例 2),直接从 head 开始移动指针,导致删除逻辑不统一。加入虚拟头结点后,所有情况都能被统一处理。

自己乱写又写错了

cpp 复制代码
/**
 * 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);
        dummy -> next = head;

        ListNode* fast = dummy;
        ListNode* slow = dummy;

        for (int i = 0 ; i < n ; i++){
            fast = fast -> next;
        }

        if (fast -> next != nullptr){
            fast = fast -> next ;
            slow = slow -> next ;
        }else{
            ListNode* temp = slow -> next ;
            slow -> next = slow -> next -> next ;
            delete temp;
        }

        ListNode* new_head = dummy -> next ;
        delete dummy ;
        return new_head;
    }
};

你把循环移动 写成了单次移动,这是最核心的错误。

好了,终于改掉了!

通过了!

通过的代码

cpp 复制代码
/**
 * 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);
        dummy -> next = head;

        ListNode* fast = dummy;
        ListNode* slow = dummy;

        for (int i = 0 ; i < n ; i++){
            fast = fast -> next;
        }
        while(fast -> next != nullptr){
            fast = fast -> next ;
            slow = slow -> next ;
        }
        ListNode* temp = slow -> next ;
        slow -> next = slow -> next -> next ;
        delete temp;

        ListNode* new_head = dummy -> next ;
        delete dummy ;
        return new_head;
    }
};

3 小结

要提取一下共性,其实一开始我担心的头结点删除,只要引入虚拟dummy,slow也会从dummy才出发,删掉的结点总是slow的下一个结点就好了,整体的还是非常简洁可读的。

还有一些细节,if语句和循环条件,整个遍历还是要再好好把握一遍!


召唤ai老师!

核心收获总结

  1. 边界问题的终极解决方案:虚拟头结点(dummy)

    • 你最初的核心困扰是 "头结点可能被删除",引入 dummy 后,无论是否删头结点,逻辑都统一为 "删除 slow 的下一个结点" (因为 slow 从 dummy 出发,即使删头结点,slow 也停在 dummy 位置,删 dummy->next 即可)。
    • 这是链表类题目处理 "头结点操作" 的通用技巧,几乎所有涉及删除 / 修改头结点的链表题都能用这个方法简化逻辑。
  2. 一趟扫描的核心:快慢指针的 "间隔法"

    • 快指针先移动 n 步,创造出和慢指针的 "固定间隔";
    • 快慢指针同步移动,直到快指针走到链表末尾(fast->next == nullptr),此时慢指针的下一个结点就是倒数第 n 个结点;
    • 你踩过的关键坑:把 while 循环写成了 if 单次判断,导致指针移动步数不足,这也提醒你 ------循环是处理 "持续移动" 的核心,条件判断仅用于单次分支
  3. 不同语言的细节差异

    • C++:需要手动 delete 释放内存(避免内存泄漏),比如删除目标结点、释放 dummy 结点;
    • JS:垃圾回收自动处理内存,无需手动释放,代码更简洁,但核心逻辑(dummy + 快慢指针)完全一致。

可复用的解题模板(链表倒数第 k 个结点类问题)

无论是找倒数第 k 个结点,还是删除倒数第 k 个结点,都可以套用这个模板:

cpp 复制代码
// 通用模板:找/删链表倒数第k个结点
ListNode* handleNthFromEnd(ListNode* head, int k) {
    // 1. 虚拟头结点
    ListNode* dummy = new ListNode(0);
    dummy->next = head;
    
    // 2. 快慢指针初始化
    ListNode* fast = dummy;
    ListNode* slow = dummy;
    
    // 3. 快指针先走k步
    for (int i = 0; i < k; i++) {
        fast = fast->next;
    }
    
    // 4. 同步移动到末尾
    while (fast->next != nullptr) {
        fast = fast->next;
        slow = slow->next;
    }
    
    // 5. 操作:找则返回 slow->next;删则删除 slow->next
    // ... 具体业务逻辑 ...
    
    // 6. 清理并返回
    ListNode* res = dummy->next;
    delete dummy;
    return res;
}

最终回顾

你从 "担心头结点删除"→"引入 dummy 解决边界"→"踩坑循环 / 条件判断"→"修正后通过",整个过程完全是自主调试、总结的优质学习路径。记住:

  • 链表题先想 "要不要加 dummy"(涉及头结点操作就加);
  • 倒数问题优先想 "快慢指针间隔法";
  • 循环和条件判断的场景要分清(持续动作用循环,单次判断用 if)。
相关推荐
Prince-Peng7 小时前
技术架构系列 - 详解Redis
数据结构·数据库·redis·分布式·缓存·中间件·架构
lxl13077 小时前
学习C++(5)运算符重载+赋值运算符重载
学习
只是懒得想了7 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
码农水水7 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
m0_736919108 小时前
模板编译期图算法
开发语言·c++·算法
dyyx1118 小时前
基于C++的操作系统开发
开发语言·c++·算法
AutumnorLiuu8 小时前
C++并发编程学习(一)——线程基础
开发语言·c++·学习
m0_736919108 小时前
C++安全编程指南
开发语言·c++·算法
CS创新实验室8 小时前
关于 Moltbot 的学习总结笔记
笔记·学习·clawdbot·molbot
蜡笔小马8 小时前
11.空间索引的艺术:Boost.Geometry R树实战解析
算法·r-tree