删除链表的倒数第 N 个结点
📌 问题简介
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
题目描述
给定一个链表,删除链表的倒数第 n 个节点,并返回链表的头节点。
提示:
- 链表中结点的数目为
sz1 <= sz <= 300 <= Node.val <= 1001 <= n <= sz
📌 示例说明
示例 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]
💡 解题思路
方法一:双指针(快慢指针)✅(推荐)
这是最经典、高效的方法,满足"一趟扫描"的要求。
步骤如下:
- 创建一个虚拟头节点
dummy,指向原链表头。这样可以统一处理删除头节点的情况。 - 定义两个指针
fast和slow,初始都指向dummy。 - 先让
fast指针向前走n + 1步(注意是n+1,因为我们要让slow停在待删除节点的前一个位置)。 - 然后
fast和slow同时向前移动,直到fast到达链表末尾(即fast == null)。 - 此时
slow指向的是倒数第n+1个节点(即待删除节点的前驱),执行slow.next = slow.next.next即可删除目标节点。 - 返回
dummy.next。
✅ 为什么是
n+1步?因为我们希望
slow最终停在要删除节点的前一个节点,这样才能修改其next指针。
方法二:先计算长度再删除 ❌(不满足进阶要求)
- 第一次遍历:计算链表总长度
L。 - 第二次遍历:从头走到第
L - n个节点(即待删除节点的前一个),然后删除。 - 时间复杂度 O(2L) ≈ O(L),但需要两趟扫描。
虽然可行,但不符合"一趟扫描"的进阶要求,不推荐。
方法三:栈(辅助空间)⚠️
- 将所有节点依次入栈。
- 弹出
n个节点,此时栈顶即为待删除节点的前驱。 - 修改指针并返回。
空间复杂度 O(L),不如双指针优雅。
💻 代码实现
java
// Java 实现:双指针法
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
// fast 先走 n+1 步
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
// fast 和 slow 同时前进,直到 fast 为 null
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 删除倒数第 n 个节点
slow.next = slow.next.next;
return dummy.next;
}
}
go
// Go 实现:双指针法
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{Val: 0, Next: head}
fast, slow := dummy, dummy
// fast 先走 n+1 步
for i := 0; i <= n; i++ {
fast = fast.Next
}
// fast 和 slow 同时前进
for fast != nil {
fast = fast.Next
slow = slow.Next
}
// 删除节点
slow.Next = slow.Next.Next
return dummy.Next
}
🧪 示例演示(以示例1为例)
原始链表:1 → 2 → 3 → 4 → 5,n = 2
- 构造虚拟头:
dummy → 1 → 2 → 3 → 4 → 5 fast先走 3 步(n+1=3):fast指向4slow在dummy,fast在4- 同步移动:
fast=5,slow=1fast=null,slow=3
- 此时
slow指向3,slow.next是4(即倒数第2个) - 执行
slow.next = slow.next.next→3 → 5 - 结果:
1 → 2 → 3 → 5✅
✅ 答案有效性证明
-
边界情况覆盖:
- 删除头节点(如
[1,2], n=2):dummy机制确保正确处理。 - 删除唯一节点(如
[1], n=1):slow指向dummy,dummy.next = null,返回null。 - 删除尾节点(如
n=1):slow停在倒数第二个,正确删除最后一个。
- 删除头节点(如
-
指针逻辑正确性:
fast走n+1步后,与slow的距离恒为n+1。- 当
fast到达null(链表尾后一位),slow必在倒数第n+1位。
因此,算法在所有合法输入下均正确。
📊 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 是否一趟扫描 |
|---|---|---|---|
| 双指针(推荐) | O(L) | O(1) | ✅ 是 |
| 计算长度 | O(L) | O(1) | ❌ 否 |
| 栈 | O(L) | O(L) | ✅ 是 |
L 为链表长度。
📌 问题总结
- 核心技巧 :使用虚拟头节点避免对头节点的特殊处理。
- 关键洞察 :通过控制两个指针的距离(
n+1),实现"定位倒数第 n 个节点的前驱"。 - 最佳实践:双指针法时间空间最优,且满足进阶要求。
- 易错点 :
- 忘记
n+1步,导致slow停在待删除节点而非其前驱。 - 未使用虚拟头,导致删除头节点时逻辑复杂。
- 忘记
💡 一句话口诀 :快指针先走 n+1,慢指针随后跟;快到终点时,慢删下一结。
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions