
一、先搞懂链表删除操作的核心本质
⚠️ 所有链表删除题的第一原则:要删除一个节点,必须先找到它的前驱节点!
比如要删除节点 4,你不能直接操作 4,必须先找到它的前一个节点 3,然后执行:
3->next = 3->next->next;
这样就把 4 从链表中删掉了。
这就是为什么这道题我们的目标不是找到倒数第 N 个节点,而是找到倒数第 N 个节点的前驱节点。
二、为什么必须用虚拟头节点?
如果要删除的节点是头节点 (比如示例 2:[1],删除倒数第 1 个),头节点没有前驱节点,需要单独写代码处理,非常容易出错。
用虚拟头节点的好处:
- 给头节点也造一个前驱,所有节点的删除逻辑完全统一,不用再单独处理头节点的情况。
三、方法一:计算链表长度
核心思路
- 先遍历一遍链表,计算出链表的总长度
L; - 倒数第 N 个节点,就是正数第
L - N + 1个节点; - 我们需要找到它的前驱,也就是正数第
L - N个节点; - 执行删除操作。
方法二:栈
核心思路
- 把所有节点(包括虚拟头)依次入栈;
- 弹出 n 个节点,此时栈顶的节点就是倒数第 N 个节点的前驱;
- 执行删除操作。
方法三:双指针法(面试首选!进阶要求,O (L) 时间,O (1) 空间)
1.核心思想(一句话记死)
让快指针先走 n 步,然后快慢指针一起走,当快指针走到链表末尾时,慢指针正好指向倒数第 n 个节点的前驱。
2. 为什么这样是对的?
- 快指针先走 n 步,此时快慢指针之间的距离正好是 n;
- 然后两个指针以相同速度一起走,距离保持不变;
- 当快指针走到
nullptr时,慢指针和末尾的距离正好是 n,也就是慢指针在倒数第 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) {
//双指针法
//两个指针
//虚拟头
ListNode *dhead = new ListNode(-1);
dhead->next = head;
ListNode *p1 = dhead;
ListNode *p2 = head;
//p2先走n步
int step = 0;//记录走了多少步
while(step<n){
p2 = p2->next;
step++;
}
//p1 p2同时走 直到p2走到头 p1的位置就是要删除的节点的前驱节点
while(p2!=nullptr){
p1 = p1->next;
p2 = p2->next;
}
p1->next = p1->next->next;
// while(p1!=nullptr){
// p1 = p1->next;
// }
return dhead->next;
}
};