力扣解题-19. 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 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 <= 30
0 <= Node.val <= 100
1 <= n <= sz
进阶:你能尝试使用一趟扫描实现吗?
Related Topics
链表、双指针
第一次解答
解题思路
核心方法:快慢双指针法(一趟扫描版),通过让快指针先移动n步,再让快慢指针同步移动,当快指针到达链表尾部时,慢指针恰好指向“倒数第n个节点的前驱节点”,直接删除目标节点,时间复杂度O(n)、空间复杂度O(1),满足进阶的“一趟扫描”要求。
核心逻辑拆解
删除倒数第n个节点的核心难点是“如何一次遍历找到倒数第n个节点的前驱”:
- 哑节点初始化:创建
dummy哑节点(dummy.next = head),解决“删除头节点”的边界问题(如示例2中删除唯一节点1); - 双指针初始化:定义
fastDummy和slowDummy均指向dummy(避免单独处理头节点); - 快指针先行n步:
- 遍历n次,将
fastDummy向后移动n步,使快慢指针之间保持n个节点的距离; - (示例1中n=2,
fastDummy从dummy→1→2,与slowDummy(dummy)间距2);
- 遍历n次,将
- 快慢指针同步移动:
- 循环条件:
fastDummy.next != null(快指针未到链表尾部); - 每次循环,
fastDummy和slowDummy各向后移动一步; - 当快指针到达尾部(
fastDummy.next = null),慢指针slowDummy恰好指向“倒数第n个节点的前驱节点”; - (示例1中最终
slowDummy指向3,其next是4(倒数第2个节点));
- 循环条件:
- 删除目标节点:
slowDummy.next = slowDummy.next.next(跳过倒数第n个节点,完成删除); - 返回结果:返回
dummy.next(跳过哑节点,得到删除后的链表头)。
具体步骤(以示例1 head=[1,2,3,4,5]、n=2为例)
| 步骤 | 操作 | fastDummy指向 | slowDummy指向 | 说明 |
|---|---|---|---|---|
| 1 | 初始化dummy→1→2→3→4→5 | dummy | dummy | 哑节点避免头节点边界问题 |
| 2 | 快指针移动1步(i=1) | 1 | dummy | 保持间距第一步 |
| 3 | 快指针移动2步(i=2) | 2 | dummy | 快指针完成n步先行 |
| 4 | 同步移动1次 | 3 | 1 | 快指针未到尾部 |
| 5 | 同步移动2次 | 4 | 2 | 快指针未到尾部 |
| 6 | 同步移动3次 | 5 | 3 | fastDummy.next=null,循环终止 |
| 7 | 删除节点 | 5 | 3 | slowDummy.next=5(跳过4) |
| 最终链表:1→2→3→5,与示例结果一致。 |
性能说明
- 时间复杂度:O(n)(仅一趟扫描,快指针移动n步 + 快慢指针同步移动(sz-n)步,总步数=sz ≤30);
- 空间复杂度:O(1)(仅使用几个指针变量,无额外存储);
- 优势:
- 一趟扫描完成所有操作,满足进阶要求,执行效率最高;
- 哑节点完美解决删除头节点的边界问题(如示例2、3);
- 双指针间距固定,逻辑直观,无冗余计算。
public ListNode removeNthFromEnd(ListNode head, int n) {
//初始化
ListNode dummy=new ListNode(0);
dummy.next=head;
//定义一个快,一个慢的栈
ListNode fastDummy=dummy;
ListNode slowDummy=dummy;
//定位到比slow快n个的栈
for(int i=1;i<=n;i++){
fastDummy=fastDummy.next;
}
//开始循环
while(fastDummy.next!=null) {
fastDummy = fastDummy.next;
slowDummy = slowDummy.next;
}
slowDummy.next=slowDummy.next.next;
return dummy.next;
}
}
示例解答
解题思路
解法1:两次遍历法(基础思路,对比参考)
核心方法:先统计长度再定位删除,第一次遍历统计链表总长度sz,第二次遍历到“sz-n”位置(倒数第n个节点的前驱),删除目标节点,逻辑更简单但需要两次扫描,时间复杂度仍为O(n),空间复杂度O(1)。
代码实现
public ListNode removeNthFromEnd(ListNode head, int n) {
// 哑节点处理边界
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode curr = dummy;
int sz = 0;
// 第一次遍历:统计链表长度
while (curr.next != null) {
sz++;
curr = curr.next;
}
// 第二次遍历:定位到倒数第n个节点的前驱(sz-n位置)
curr = dummy;
for (int i = 0; i < sz - n; i++) {
curr = curr.next;
}
// 删除目标节点
curr.next = curr.next.next;
return dummy.next;
}
适用场景说明
- 时间复杂度:O(n)(两次遍历,总步数=2sz);
- 空间复杂度:O(1);
- 优势:逻辑极简,新手易理解,无需理解双指针间距的逻辑;
- 劣势:需要两次扫描链表,效率略低于双指针法,不满足进阶要求。
解法2:栈辅助法(思路拓展)
核心方法:利用栈的“后进先出”特性,将所有节点入栈,再出栈n次,栈顶元素即为倒数第n个节点的前驱,删除目标节点,逻辑直观但需要额外空间。
代码实现
import java.util.Stack;
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
Stack<ListNode> stack = new Stack<>();
ListNode curr = dummy;
// 所有节点入栈
while (curr != null) {
stack.push(curr);
curr = curr.next;
}
// 出栈n次,找到倒数第n个节点的前驱
for (int i = 0; i < n; i++) {
stack.pop();
}
// 栈顶是前驱节点,删除目标节点
ListNode prev = stack.peek();
prev.next = prev.next.next;
return dummy.next;
}
性能说明
- 时间复杂度:O(n)(入栈+出栈共2n步);
- 空间复杂度:O(n)(栈存储所有节点);
- 优势:无需计算指针间距,利用栈的特性直接定位前驱,逻辑易理解;
- 劣势:额外使用栈存储节点,空间开销高于双指针法,不推荐在工程中使用。
总结
- 快慢双指针法(第一次解答):O(n)时间+O(1)空间,一趟扫描完成,满足进阶要求,是工程首选的最优解法;
- 两次遍历法:O(n)时间+O(1)空间,逻辑极简,适合新手理解核心思路但效率略低;
- 栈辅助法:O(n)时间+O(n)空间,思路拓展但有额外空间开销,仅作参考;
- 关键技巧:
- 核心思想:双指针间距固定n步,一次遍历定位目标节点前驱,避免多次扫描;
- 边界处理:哑节点是解决“删除头节点”的关键,所有解法均需优先使用;
- 进阶优化:双指针法是链表“倒数第k个节点”类问题的通用最优解法,需重点掌握。