【力扣100题】15.删除链表的倒数第 N 个结点

1. 题目描述

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

示例

复制代码
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
解释:删除倒数第2个结点(值为4)

输入:head = [1], n = 1
输出:[]
解释:删除倒数第1个结点(唯一的结点),链表为空

输入:head = [1,2], n = 1
输出:[1]
解释:删除倒数第1个结点(值为2)

提示

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

进阶

你能尝试使用一趟扫描实现吗?


2. 核心思想

关键思想:快慢指针 + 虚拟头节点

要删除倒数第 n 个结点,需要找到它的前一个结点

核心思路:让快指针先走 n 步,然后快慢指针一起走,当快指针走到末尾时,慢指针正好在目标结点的前一个位置。

复制代码
以 [1,2,3,4,5], n=2 为例:

初始:dummy → 1 → 2 → 3 → 4 → 5 → nullptr
             ↑           ↑
             slow        fast

第1步:fast 走 n=2 步
       fast = 3(走了1步)
       fast = 4(走了2步)

第2步:fast 和 slow 同时走
       fast=4, slow=1  → fast->next=5, slow=2
       fast=5, slow=2  → fast->next=nullptr, slow=3
       fast->next=null,退出

此时:slow 在结点 3 的位置
      slow->next 是结点 4(要删除的)
      slow->next = slow->next->next,删除成功!

3. 多种方法解决

方法一:快慢指针 + 虚拟头节点(推荐)✅

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, head);  // 虚拟头节点
        ListNode* fast = dummy;
        
        // 第一步:快指针先走 n 步
        while (n--) {
            fast = fast->next;
        }
        
        // 第二步:快慢指针同时走
        ListNode* slow = dummy;
        while (fast->next) {
            slow = slow->next;
            fast = fast->next;
        }
        
        // 第三步:删除目标结点
        ListNode* toDelete = slow->next;
        slow->next = slow->next->next;
        delete toDelete;
        
        return dummy->next;
    }
};

复杂度: 时间 O(L),空间 O(1),只遍历一次 ✅


方法二:计算链表长度(两次遍历)

cpp 复制代码
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        
        // 第一次遍历:计算链表长度
        int length = 0;
        ListNode* curr = head;
        while (curr) {
            length++;
            curr = curr->next;
        }
        
        // 第二次遍历:找到倒数第 n 个结点的前一个
        length -= n;  // 正数第 length-n+1 个就是要删的
        curr = dummy;
        while (length-- > 1) {
            curr = curr->next;
        }
        
        // 删除
        ListNode* toDelete = curr->next;
        curr->next = curr->next->next;
        delete toDelete;
        
        return dummy->next;
    }
};

复杂度: 时间 O(L),空间 O(1),但需要遍历两次 ❌


方法三:栈(不推荐)

cpp 复制代码
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        stack<ListNode*> stk;
        
        ListNode* curr = dummy;
        while (curr) {
            stk.push(curr);
            curr = curr->next;
        }
        
        // 弹出 n 个,剩下的栈顶就是要删结点的前一个
        for (int i = 0; i < n; i++) {
            stk.pop();
        }
        
        ListNode* prev = stk.top();
        prev->next = prev->next->next;
        
        return dummy->next;
    }
};

复杂度: 时间 O(L),空间 O(L) ❌


4. 图解过程

示例:head = 1,2,3,4,5, n = 2

复制代码
初始状态:
dummy(0) → 1 → 2 → 3 → 4 → 5 → nullptr
 ↑
fast=dummy, slow=dummy

第一步:fast 先走 n=2 步
       fast = 0→next = 1(走1步)
       fast = 1→next = 2(走2步)

第二步:fast 和 slow 同时走,直到 fast→next == nullptr
       
       fast=2, slow=dummy
       fast->next = 3 ≠ nullptr,继续
       
       fast=3, slow=1
       fast->next = 4 ≠ nullptr,继续
       
       fast=4, slow=2
       fast->next = 5 ≠ nullptr,继续
       
       fast=5, slow=3
       fast->next = nullptr,退出

此时:
slow = 3,slow->next = 4(要删除的结点)

删除操作:
slow->next = slow->next->next
即:3→next = 5

最终:
dummy → 1 → 2 → 3 → 5 → nullptr

边界情况:head = 1, n = 1

复制代码
初始:dummy(0) → 1 → nullptr

fast 先走1步:
fast = 1(走完)

fast->next = nullptr,while 不执行

slow = dummy
slow->next = 1(要删除)

删除后:
dummy → nullptr
return dummy->next = nullptr ✓

5. 方法优缺点比较

方法 时间 空间 优点 缺点
快慢指针 O(L) ✅ O(1) ✅ 一趟扫描,指针移动最少 思维稍难
计算长度 O(L) O(1) 简单直观 需要两次遍历
O(L) O(L) 代码简单 空间开销大

推荐方法

方法一(快慢指针) 是面试和竞赛中的标准解法:

  1. 一趟扫描,时间最优
  2. O(1) 空间
  3. 掌握后代码很简洁

6. 完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

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);
        ListNode* fast = dummy;
        
        while (n--) {
            fast = fast->next;
        }
        
        ListNode* slow = dummy;
        while (fast->next) {
            slow = slow->next;
            fast = fast->next;
        }
        
        ListNode* toDelete = slow->next;
        slow->next = slow->next->next;
        delete toDelete;
        
        return dummy->next;
    }
};

// 辅助函数:链表转 vector(方便打印)
vector<int> toVector(ListNode* head) {
    vector<int> result;
    while (head) {
        result.push_back(head->val);
        head = head->next;
    }
    return result;
}

// 辅助函数:vector 转链表
ListNode* toList(vector<int> vals) {
    ListNode* dummy = new ListNode(0);
    ListNode* curr = dummy;
    for (int v : vals) {
        curr->next = new ListNode(v);
        curr = curr->next;
    }
    return dummy->next;
}

int main() {
    Solution sol;

    // 测试1:[1,2,3,4,5], n=2 → [1,2,3,5]
    ListNode* h1 = toList({1, 2, 3, 4, 5});
    vector<int> r1 = toVector(sol.removeNthFromEnd(h1, 2));
    cout << "[1,2,3,4,5], n=2 → [";
    for (int i = 0; i < r1.size(); i++)
        cout << r1[i] << (i < r1.size()-1 ? "," : "");
    cout << "]" << endl;

    // 测试2:[1], n=1 → []
    ListNode* h2 = toList({1});
    vector<int> r2 = toVector(sol.removeNthFromEnd(h2, 1));
    cout << "[1], n=1 → [";
    for (int i = 0; i < r2.size(); i++)
        cout << r2[i] << (i < r2.size()-1 ? "," : "");
    cout << "]" << endl;

    // 测试3:[1,2], n=1 → [1]
    ListNode* h3 = toList({1, 2});
    vector<int> r3 = toVector(sol.removeNthFromEnd(h3, 1));
    cout << "[1,2], n=1 → [";
    for (int i = 0; i < r3.size(); i++)
        cout << r3[i] << (i < r3.size()-1 ? "," : "");
    cout << "]" << endl;

    return 0;
}

输出:

复制代码
[1,2,3,4,5], n=2 → [1,2,3,5]
[1], n=1 → []
[1,2], n=1 → [1]

8. 总结

要点 说明
核心思想 快慢指针:快指针先走 n 步,然后一起走到末尾
虚拟头节点 统一处理头结点删除,无需单独判断
关键 slow 停在目标结点的前一个位置
复杂度 时间 O(L),空间 O(1),一趟扫描 ✅
相关推荐
刘广睿5 分钟前
素材去重为什么难?文件指纹、感知哈希与相似度判定的设计笔记
算法·哈希算法·产品设计·素材管理
少司府18 分钟前
C++进阶:智能指针
开发语言·数据结构·c++·b树·算法·c·智能指针
鹿角片ljp34 分钟前
LeetCode 22:括号生成|回溯dfs、剪枝与 char[] 覆盖代替撤销
算法·深度优先
sylviiiiiia1 小时前
leetcode hot100
python·算法·leetcode
纪伊路上盛名在1 小时前
Kabsch算法的Julia实现
开发语言·算法·julia·序列分析·蛋白质·rmsd
裕晟资质规划1 小时前
军工保密资质二级申报的四个可量化硬条件:条文位置、数值口径与西安配套企业实务要点
java·服务器·网络·数据库·算法
LuminousCPP1 小时前
数据结构-排序(一):基础排序算法横向对比|冒泡、插入、希尔、堆与双向选择,详解二分 / Knuth 增量
c语言·数据结构·笔记·算法·排序算法
Nil2081 小时前
leetcode 46全排列
算法·leetcode·职场和发展
辰烨chenye2 小时前
LeetCode Hot 100 题解 · 动态规划篇
算法·leetcode·动态规划
青山木2 小时前
Hot 100 --- 买卖股票的最佳时机
java·数据结构·算法·leetcode·贪心算法