1 题目
给你一个链表,删除链表的倒数第 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 <= 300 <= Node.val <= 1001 <= 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 个结点的问题,并且希望用一趟扫描实现,核心困扰是不知道如何处理头结点被删除的边界情况,你写的代码也因为没考虑这个问题导致错误。
正确的解题思路(双指针 + 虚拟头结点)
- 虚拟头结点(dummy):解决删除头结点时的边界问题,让所有结点的删除逻辑统一。
- 快慢指针 :
- 快指针先向前走
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;
}
};
代码关键部分解释
- 虚拟头结点
dummy:- 比如输入是
[1]、n=1时,原头结点 1 要被删除,若没有dummy,直接操作head会无法处理;有了dummy后,slow会停在dummy位置,删除dummy->next即可。 - 所有结点的删除逻辑都统一为 "删除慢指针的下一个结点",无需单独判断是否删头结点。
- 比如输入是
- 快慢指针的移动 :
- 快指针先移
n步,保证快慢指针的间隔是n。 - 一起移动到快指针末尾时,慢指针的下一个就是目标结点(比如示例 1 中,快指针移 2 步到 3,然后一起移到快指针到 5 的下一个(nullptr),慢指针停在 3,下一个 4 就是要删的)。
- 快指针先移
- 内存释放 :
delete temp和delete 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]
总结
- 核心技巧 :用虚拟头结点 解决删除头结点的边界问题,用双指针(快慢指针) 实现一趟扫描找到倒数第 N 个结点。
- 关键逻辑 :快指针先移
n步,快慢指针再同步移动,直到快指针到末尾,此时慢指针的下一个就是要删除的结点。 - 内存管理 :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老师!
核心收获总结
-
边界问题的终极解决方案:虚拟头结点(dummy)
- 你最初的核心困扰是 "头结点可能被删除",引入
dummy后,无论是否删头结点,逻辑都统一为 "删除 slow 的下一个结点" (因为 slow 从 dummy 出发,即使删头结点,slow 也停在 dummy 位置,删dummy->next即可)。 - 这是链表类题目处理 "头结点操作" 的通用技巧,几乎所有涉及删除 / 修改头结点的链表题都能用这个方法简化逻辑。
- 你最初的核心困扰是 "头结点可能被删除",引入
-
一趟扫描的核心:快慢指针的 "间隔法"
- 快指针先移动
n步,创造出和慢指针的 "固定间隔"; - 快慢指针同步移动,直到快指针走到链表末尾(
fast->next == nullptr),此时慢指针的下一个结点就是倒数第n个结点; - 你踩过的关键坑:把
while循环写成了if单次判断,导致指针移动步数不足,这也提醒你 ------循环是处理 "持续移动" 的核心,条件判断仅用于单次分支。
- 快指针先移动
-
不同语言的细节差异
- C++:需要手动
delete释放内存(避免内存泄漏),比如删除目标结点、释放 dummy 结点; - JS:垃圾回收自动处理内存,无需手动释放,代码更简洁,但核心逻辑(dummy + 快慢指针)完全一致。
- C++:需要手动
可复用的解题模板(链表倒数第 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)。