Hot 100 --- 删除链表的倒数第 N 个结点
本文概览:本文以LeetCode经典题目"删除链表的倒数第 N 个结点"为例,从链表单向遍历的限制入手,分析为什么倒数删除不能像正序删除一样直接计数,再引出前后双指针保持固定距离的解法
一、题目

二、题目分析
给定一个链表,删除链表的倒数第 n 个结点,并返回链表的头结点
目标:找到倒数第 n 个节点并删除它
核心难点:链表是单向的,只能从前往后遍历,不能从尾节点往前数
思路概览
Java实现代码如下
1 | public ListNode removeNthFromEnd(ListNode head, int n) { |
思路简要说明
前后双指针:用
front指向可能被删除的节点,用behind指向更靠后的节点保持固定距离:先让
behind走n-1步,使behind和front之间保持n-1个节点距离同步移动:两个指针一起向前走,当
behind到达最后一个节点时,front正好指向倒数第n个节点删除节点:用
prev记录front的前一个节点,最后让prev.next = front.next
三、思路详解
为什么倒数删除更麻烦?
如果题目是删除正数第 n 个节点,那很简单:从头节点开始向前走,用一个计数器数到第 n 个节点即可
但是现在要删除的是倒数第 n 个节点。倒数意味着它的位置是从链表尾部往前数出来的,而链表是单向的,我们无法从尾节点反向走回来
比如链表:
1 | 1 → 2 → 3 → 4 → 5 |
删除倒数第 2 个节点,也就是删除 4。站在尾节点 5 的视角看,往前走 1 步就是 4。但问题是:链表没有 prev 指针,尾节点 5 无法往前走到 4
所以这道题的核心矛盾就是:目标节点由尾部决定,但链表只能从头部向后遍历
暴力解法入手
最直观的想法有两种:
方法一:先计算链表长度
先遍历一遍链表,得到链表长度 len。倒数第 n 个节点,其实就是正数第 len - n + 1 个节点。然后再遍历一遍链表,找到这个节点并删除
- 时间复杂度:O(n),但是需要两次遍历
- 空间复杂度:O(1)
- 问题:虽然复杂度是 O(n),但需要先知道长度,整体过程不够直接
方法二:反转链表再删除
既然倒数不好找,那可以把链表反转。反转后,原来的倒数第 n 个节点就变成了正数第 n 个节点,再删除就容易了
但这个思路也有明显问题:
- 需要先反转链表
- 删除后如果要保持原链表方向,还要再反转回来
- 操作链表指针更多,容易写复杂
所以我们需要一个更自然的方法:能不能不反转链表,也不先计算完整长度,而是在一次遍历中找到倒数第 n 个节点?
双指针解法
核心思路
倒数第 n 个节点,本质上和尾节点之间有一个固定距离
如果要删除倒数第 n 个节点,那么它距离尾节点是 n-1 步:
1 | 1 → 2 → 3 → 4 → 5 |
既然目标节点和尾节点之间有固定距离,那我们可以让两个指针从头开始,提前制造出这个距离
具体做法:
front和behind都从头节点出发- 先让
behind走n-1步 - 然后
front和behind一起走 - 当
behind到达链表最后一个节点时,front正好指向倒数第n个节点
为什么是让 behind 先走 n-1 步?
因为倒数第 n 个节点到最后一个节点之间正好隔着 n-1 条边
例如删除倒数第 3 个节点:
1 | 1 → 2 → 3 → 4 → 5 |
所以只要让两个指针始终保持 n-1 的距离,当后指针到达最后一个节点时,前指针自然就是倒数第 n 个节点
为什么需要 prev?
单链表删除一个节点,必须拿到它的前一个节点,因为删除操作本质是:
1 | prev.next = front.next; |
如果只知道要删除的节点 front,但不知道它前面的节点,就无法让前一个节点跳过它
所以在两个指针同步移动时,用 prev 记录 front 的前一个节点:
1 | prev = front; |
这样最后 front 指向要删除的节点,prev 就指向它的前一个节点
特殊情况:删除头节点
如果要删除的是头节点,例如:
1 | 1 → 2 → 3 |
此时 front 一开始就是 head,最终也会指向 head,而 prev 仍然是 null,因为 head 前面没有节点
所以代码中需要单独判断:
1 | if (prev != null) { |
如果 prev == null,说明删除的是头节点,直接让 head = head.next 即可
举例说明
以 head = 1 → 2 → 3 → 4 → 5,n = 2 为例
第一步:behind 先走 n-1 = 1 步
1 | 1 → 2 → 3 → 4 → 5 |
第二步:front 和 behind 同步移动
| 步骤 | front | behind | prev |
|---|---|---|---|
| 初始 | 1 | 2 | null |
| 1 | 2 | 3 | 1 |
| 2 | 3 | 4 | 2 |
| 3 | 4 | 5 | 3 |
此时 behind.next == null,说明 behind 到达最后一个节点,front 指向 4,也就是倒数第 2 个节点,prev 指向 3
第三步:删除 front
1 | 删除前:1 → 2 → 3 → 4 → 5 |
最终返回 1 → 2 → 3 → 5
- 时间复杂度:O(n),链表只遍历一次
- 空间复杂度:O(1),只使用几个指针变量


