📌 题目链接: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]。
🧠 核心算法及代码讲解
✅ 推荐方法:双指针法(快慢指针)
这是本题的最优解法,满足“一趟扫描完成”的进阶要求。
🔍 算法思想:
使用两个指针
first和second,让first先走n步,然后两者同时前进。当first到达末尾时,second恰好指向待删除节点的前驱节点。
这样就可以直接进行删除操作:second->next = second->next->next;
✅ 为什么可行?
first超前n步 → 两指针之间相隔n个节点。- 当
first走完链表,second自然落在倒数第n个节点的前面。 - 无需计算链表长度,也无需额外空间。
🛠️ 技巧:引入哑节点(dummy node)
- 解决删除头节点的特殊情况。
- 统一所有情况的处理逻辑,避免写 if-else 分支。
💡 哑节点是链表题中的“神器”,在面试中经常出现!
🧩 解题思路(分步解析)
-
创建哑节点
ListNode* dummy = new ListNode(0, head);dummy->next = head- 无论删除哪个节点,都能通过
dummy安全地获取前驱。
-
初始化双指针
first = head(快指针)second = dummy(慢指针)
-
让 first 先走 n 步
- 循环
n次,first = first->next - 此时
first比second超前n个位置。
- 循环
-
双指针同步移动
- 同时移动
first和second,直到first == nullptr - 此时
second指向的是待删节点的前驱
- 同时移动
-
执行删除操作
second->next = second->next->next; -
返回结果
return dummy->next; -
释放内存(可选)
- 实际面试中需与面试官确认是否释放被删节点内存。
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 是否一次遍历 | 是否推荐 |
|---|---|---|---|---|
| 计算长度 + 遍历 | 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 << " ";
result1 = result1->next;
}
cout << endl;
// 🧪 测试用例 2: [1], n=1 -> []
ListNode* head2 = new ListNode(1);
ListNode* result2 = sol.removeNthFromEnd(head2, 1);
if (result2 == nullptr) cout << "[]\n";
else {
while (result2) {
cout << result2->val << " ";
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 << " ";
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]🔹 核心思路:递归或迭代方式,每次交换两个节点,注意维护前后连接关系。
🔹 考点:链表操作、指针重连、递归思维、边界处理(奇偶长度)。
🔹 难度:中等,但非常经典,常出现在大厂面试中!
💡 提示:可以尝试用递归和迭代两种方式实现,对比它们的优劣!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!