【力扣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),一趟扫描 ✅
相关推荐
LTphy2 小时前
深度优先搜索的三种模板
算法·深度优先·图论
paeamecium2 小时前
【PAT甲级真题】- Insert or Merge (25)
数据结构·c++·算法·排序算法·pat考试·pat
不爱吃炸鸡柳2 小时前
6道经典算法题详解:从排序到链表,覆盖面试高频考点
算法·链表·面试
wfbcg2 小时前
每日算法练习:LeetCode 3. 无重复字符的最长子串 ✅
算法·leetcode·职场和发展
_日拱一卒2 小时前
LeetCode:矩阵置零
java·数据结构·线性代数·算法·leetcode·职场和发展·矩阵
穿条秋裤到处跑2 小时前
每日一道leetcode(2026.04.10):三个相等元素之间的最小距离 I
算法·leetcode
nlpming2 小时前
OpenClaw 代码解析
算法
学习永无止境@3 小时前
MATLAB中矩阵转置
算法·matlab·fpga开发·矩阵
七颗糖很甜3 小时前
雨滴谱数据深度解析——从原始变量到科学产品的Python实现【下篇】
python·算法·pandas