文章目录
-
- 题目描述
- 核心难点与通用技巧
-
- 难点分析
- 核心技巧:哨兵节点(Dummy Node)
- 解题思路总览
- 最佳实践:双指针法(一次遍历)
-
- 原理讲解
- 详细步骤流程
- 算法流程图
- 过程时序演示 (以 1->2->3->4->5, n=2 为例)
- Java 代码实现
- 复杂度分析
-
- 时间复杂度
- 空间复杂度
- 总结
在链表操作中,这是一道非常经典的题目。它考察了我们对链表特性的理解,特别是如何在无法预知链表长度且只能单向遍历的情况下,精准定位目标节点。
题目描述
给你一个链表的头结点 head 和一个整数 n,请你删除链表中倒数第 n 个结点,并返回链表的头结点。
示例:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
核心难点与通用技巧
难点分析
单向链表只能向后遍历,无法像数组一样通过索引直接访问,也无法像双向链表一样从尾部向前遍历。要删除"倒数"第 N 个节点,我们必须找到该节点的前驱节点(即倒数第 N+1 个节点)。
核心技巧:哨兵节点(Dummy Node)
在处理链表删除操作时,最容易出现问题的就是删除头结点 的情况。如果倒数第 N 个刚好是头结点,我们需要特殊的逻辑来更新 head。
为了简化代码,通用的技巧是创建一个 dummy 节点(哑节点/哨兵节点),将其 next 指向 head。这样,无论删除的是哪个节点,我们都有一个确定的"前驱节点",最后只需返回 dummy.next 即可。
引入哨兵节点
Dummy
1
2
3
解题思路总览
我们可以通过以下思维导图来梳理主要的解题策略:
删除倒数第N个节点
计算链表长度法
原理: 先遍历求长度L,再走L-n步
优点: 直观,易理解
缺点: 需要两次遍历
一次遍历
原理: 快慢指针维护固定间距
优点: 效率高,只需一次遍历
关键: 快指针先走n+1步
栈辅助法
原理: 利用栈的先进后出特性
优点: 逻辑简单
缺点: O(N)的额外空间
考虑到面试和工程实践中的最优解,本文将重点讲解 双指针法(快慢指针),这是本题的"标准答案"。
最佳实践:双指针法(一次遍历)
原理讲解
我们要找到倒数第 n 个节点的前一个节点。我们可以利用两个指针 fast 和 slow,让它们之间保持一定的"距离"。
想象一把尺子,长度为 n。
- 让
fast指针先出发。 - 当
fast走了n步(或者n+1步,取决于实现细节)之后,slow指针从头开始出发。 - 两个指针以相同的速度向后移动。
- 当
fast指针到达链表末尾(null)时,由于两者距离恒定,slow指针刚好停留在我们需要的位置。
详细步骤流程
为了方便删除,我们需要让 slow 指针停在 倒数第 n+1 个节点(即目标节点的前驱)。
- 初始化
dummy节点指向head。 - 初始化
fast和slow都指向dummy。 - 关键步骤 :让
fast先向后移动n + 1步。 - 同时移动
fast和slow,直到fast指向null。 - 此时
slow指向的正是待删除节点的前驱节点。 - 执行删除操作:
slow.next = slow.next.next。
算法流程图
否
是
开始
初始化 Dummy 节点指向 Head
Fast, Slow 均指向 Dummy
Fast 指针先向前移动 n+1 步
Fast 是否为 null?
Fast 和 Slow 同时前移 1 步
Slow 到达目标节点的前驱
删除操作: slow.next = slow.next.next
返回 dummy.next
过程时序演示 (以 1->2->3->4->5, n=2 为例)
我们来看一下指针的移动过程:
Null Node 5 Node 4 Node 3 Node 2 Node 1 Dummy Null Node 5 Node 4 Node 3 Node 2 Node 1 Dummy 初始状态: Fast=Dummy, Slow=Dummy, n=2 第一阶段: Fast 先走 n+1 (3) 步 第二阶段: 两者同速移动 loop [直到 Fast == Null] 此时 Slow 在 Node 3 (倒数第3个节点) 删除 Node 4 (3.next = 5) Fast 移动到 Node 3 Fast 移至 4 Slow 移至 1 Fast 移至 5 Slow 移至 2 Fast 移至 Null Slow 移至 3
Java 代码实现
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 1. 创建哨兵节点,处理删除头结点的极端情况
ListNode dummy = new ListNode(0, head);
// 2. 初始化快慢指针
ListNode fast = dummy;
ListNode slow = dummy;
// 3. 让 fast 指针先走 n+1 步
// 为什么要走 n+1 步?
// 因为我们希望 slow 最后停在倒数第 n+1 个节点(即待删除节点的前驱)
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
// 4. 同时移动两个指针,直到 fast 到达末尾
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 5. 此时 slow 指向的是待删除节点的前一个节点
// 执行删除操作
slow.next = slow.next.next;
// 6. 返回新的头结点
return dummy.next;
}
}
复杂度分析
时间复杂度
O(L),其中 L 是链表的长度。
fast指针遍历了链表一次。slow指针遍历了链表L-n次。- 总体来说,我们对链表进行了一次遍历操作。
空间复杂度
O(1)。
- 我们需要常数级别的额外空间来存储
dummy、fast和slow变量,没有使用随链表长度增长的额外结构(如数组或栈)。
总结
解决"删除链表倒数第 N 个节点"的关键点在于:
- 哨兵节点 (Dummy Node) :极大地简化了边界条件的处理,特别是当需要删除头节点时,不需要单独编写
if逻辑。 - 双指针 (Two Pointers) :利用
fast和slow之间的固定间距(Gap),将"寻找倒数第 N 个"转化为"寻找正数第 L-N 个",且无需预先计算长度 L。
这种快慢指针/双指针的技巧在链表题目中非常常用(例如找中点、判断环),掌握它对于提升算法能力至关重要。