一、题目大意
给定一个链表,删除链表的倒数第 n 个结点,返回链表的头结点。 示例: 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5],删除倒数第 2 个节点 4。 特殊边界:
- 链表只有一个节点
[1],n=1→输出空链表; - 删除链表第一个节点。
要求:最好一趟遍历完成;如果不使用 dummy,删除头节点要单独 if 分支判断。
二、解题思路
核心思想:虚拟头结点 + 快慢指针
- 虚拟头结点 dummy:
dummy.next = head。统一删除逻辑,不用单独处理删除头结点的特殊情况。 - 快慢指针
slow、fast全部初始指向dummy。 - fast 指针先走 n 步,制造快慢指针之间 n 个节点的间隔。
- 循环条件:
while(fast.next != null),fast 和 slow 同步向后移动。 - 循环结束,slow 停在待删除节点的前驱节点。
删除链表节点,必须拿到前驱节点,执行:
slow.next = slow.next.next。
- 返回
dummy.next。
- 时间复杂度:O (L),L 链表长度,只遍历一次链表
- 空间复杂度:O (1),仅使用几个指针变量
三、完整 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) {
ListNode dummy = new ListNode(-1);
dummy.next = head; //虚拟节点接上原链表
ListNode slow = dummy;
ListNode fast = dummy;
//fast先走n步
for(int i = 0; i < n; i++){
fast = fast.next;
}
//fast后面还有节点,快慢同步向后移动
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
//slow为待删节点前驱,执行删除
slow.next = slow.next.next;
return dummy.next;
}
}
四、实例演示
样例 1:head=[1,2,3,4,5],n=2
链表结构:dummy(-1) →1→2→3→4→5
| 步骤 | fast 位置 | fast.next | slow 位置 | 操作说明 |
|---|---|---|---|---|
| 初始 | -1(dummy) | 1 | -1(dummy) | 快慢指针指向 dummy |
| fast 第 1 次先走 | 1 | 2 | -1(dummy) | i=0 |
| fast 第 2 次先走 | 2 | 3 | -1(dummy) | i=1,n 步走完 |
| 同步移动① | 3 | 4 | 1 | fast.next≠null,一起后移 |
| 同步移动② | 4 | 5 | 2 | fast.next≠null,一起后移 |
| 同步移动③ | 5 | null | 3 | fast.next==null,退出循环 |
循环结束,slow = 节点 3(待删节点 4 的前驱);执行slow.next = slow.next.next,节点 3 直接指向 5,删除节点 4。 输出链表:[1,2,3,5]
边界样例 2:head=[1], n=1
链表:dummy(-1) →1
| 步骤 | fast 位置 | fast.next | slow 位置 | 操作说明 |
|---|---|---|---|---|
| 初始 | -1(dummy) | 1 | -1(dummy) | 快慢指针指向 dummy |
| fast 先走 1 步 | 1 | null | -1(dummy) | n=1 步完成 |
| 循环判断 | null | null | -1(dummy) | fast.next 为 null,循环不进入 |
slow 仍然在 dummy;执行slow.next = slow.next.next,dummy.next = null,返回空链表。
五、语法坑与边界总结
-
必须写
dummy.next = head,虚拟头结点要接上原链表,否则链表为空,直接空指针。 -
循环条件和 fast 先走步数必须配对
- ❌错误组合:fast 先走 n 步,写
while(fast != null);遇到删除头节点样例,slow 跑到待删节点本身,slow.next为 null,访问slow.next.next报空指针。
- ❌错误组合:fast 先走 n 步,写
-
删除语句:
slow.next = slow.next.next- ❌错误写法:
slow = slow.next.next,只移动 slow 指针,没有修改链表连接,节点不会被删除。
- ❌错误写法:
-
slow 最终位置要求:待删除节点的前驱节点,不是待删除节点本身。
-
返回值永远返回
dummy.next,不能返回 head。 -
边界测试用例:
- 链表只有单个节点;
- 删除链表第一个节点;
- 删除链表末尾节点。
六、个人感悟
- 遇到链表可能删除头节点的场景,优先考虑虚拟头结点
dummy,省去大量 if 分支判断。 - 快慢指针做题,重点不是单纯移动指针,要思考循环结束之后指针停在哪一个位置。
- 链表删除节点的核心逻辑:一定要拿到待删节点的前一个节点,通过修改 next 引用实现删除。
- 两套快慢指针写法都可以 AC,优先选用
fast先走n步 + while(fast.next != null),边界样例不容易出错。