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) | 代码简单 | 空间开销大 |
推荐方法
方法一(快慢指针) 是面试和竞赛中的标准解法:
- 一趟扫描,时间最优
- O(1) 空间
- 掌握后代码很简洁
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),一趟扫描 ✅ |