
一、题目概述
题目要求 :给定一个单向链表的头节点 head,删除链表的倒数第 n 个节点,并返回删除后的链表头节点。
解题难点:
- 单向链表无法逆向遍历,不能直接从末尾往前找节点;
- 常规解法需要先遍历一次统计链表长度,再二次遍历删除节点,效率较低;
- 存在特殊边界:当需要删除链表第一个节点时,直接操作头节点会出现空指针、节点丢失问题。
最优解题思路 :虚拟头节点 + 快慢指针(双指针)一次遍历法,仅遍历链表一次即可完成删除,时间复杂度最优。
二、核心算法原理
1. 虚拟头节点(dummy)作用
这是解决本题边界问题的关键。我们手动在链表最前端新增一个值为 0 的虚拟节点,让虚拟节点的后继指向原链表头节点。
核心价值:统一所有节点的删除逻辑。无论删除的是链表头节点、中间节点、尾节点,待删除节点都存在前驱节点,彻底避免了删除头节点时无前置节点的特殊情况,无需单独写边界判断代码。
2. 快慢指针核心逻辑
通过两个指针的步数差,精准定位倒数第 n 个节点的前驱节点:
- 快指针、慢指针初始均指向虚拟头节点;
- 快指针先单独向后移动 n 步,让快慢指针之间形成 n 个节点的固定间距;
- 快慢指针同时向后同步移动,直到快指针走到链表最后一个节点;
- 此时慢指针精准停在倒数第 n 个节点的前一个节点;
- 修改慢指针的后继指针,跳过待删除节点,完成删除操作。
三、完整代码逐行精讲
ini
/**
* Definition for singly-linked list.
* 链表节点构造函数:默认值0,默认后继为null
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head 原链表头节点
* @param {number} n 需要删除的倒数节点序号
* @return {ListNode} 删除后的链表头节点
*/
var removeNthFromEnd = function(head, n) {
// 1. 创建虚拟头节点,挂载在原链表最前端
const dummy = new ListNode(0, head);
// 2. 初始化快慢指针,均指向虚拟头节点
let slow = dummy, fast = dummy;
// 3. 快指针先走n步,构建n个节点的间距
while(n--){
fast = fast.next;
}
// 4. 快慢指针同步后移,直到快指针到达链表末尾
while(fast.next && slow.next){
fast = fast.next;
slow = slow.next;
}
// 5. 删除slow的下一个节点(倒数第n个节点)
slow.next = slow.next.next;
// 6. 返回新链表的真实头节点(跳过虚拟节点)
return dummy.next;
};
逐行详细解析
1. 创建虚拟头节点
const dummy = new ListNode(0, head);
生成一个值为 0 的空节点,该节点的后继直接指向原链表头节点。此时链表结构变为:dummy -> 原链表所有节点。
2. 初始化双指针
let slow = dummy, fast = dummy;
快慢指针初始位置统一,都指向虚拟头节点,保证初始状态一致,为后续拉开间距做铺垫。
3. 快指针先行 n 步
while(n--){fast = fast.next;}
n-- 是先判断后自减,循环精准执行 n 次,快指针向后移动 n 个位置。执行完成后,快慢指针的间隔严格为 n 个节点,这是精准定位的核心。
4. 双指针同步遍历至链表末尾
while(fast.next&&slow.next){fast = fast.next;slow = slow.next;}
循环条件 fast.next 表示:只要快指针不是最后一个节点,就继续移动。
当循环结束时,快指针停在链表最后一个有效节点,慢指针恰好停在倒数第 n 个节点的前驱节点。
补充:slow.next 为冗余安全校验,防止极端空指针报错,不影响正常逻辑。
5. 执行节点删除
slow.next = slow.next.next;
直接让慢指针的后继,指向「待删除节点的后继节点」,断开待删除节点的链表关联,节点自动被回收,完成删除。
6. 返回新链表头节点
return dummy.next;
不能返回原 head!若原链表头节点被删除,原 head 已失效。dummy.next 永远指向删除后的真实链表头节点,适配所有场景。
四、完整流程实例演示
测试案例 :链表[1,2,3,4,5],删除倒数第 2 个节点(即节点 4)
-
构建虚拟节点:
dummy(0) -> 1 -> 2 -> 3 -> 4 -> 5 -
初始指针:
slow = dummy、fast = dummy -
快指针先走 2 步:fast 依次走到 1、2,此时快慢指针间距为 2
-
同步遍历:
- fast=2、slow=0 → fast=3、slow=1
- fast=3、slow=1 → fast=4、slow=2
- fast=4、slow=2 → fast=5、slow=3
-
此时 fast.next = null,循环终止,slow 停在节点 3(待删除节点4的前驱)
-
执行删除:
3.next = 4.next,链表变为0->1->2->3->5 -
返回
dummy.next,最终结果:[1,2,3,5]
五、核心边界情况全覆盖测试
边界1:单节点链表(删除唯一节点)
输入:head=[1], n=1
流程:dummy->1 → fast先走1步到1 → fast.next为null,循环不执行 → slow=dummy → dummy.next=null,返回空链表,结果正确。
边界2:删除链表头节点
输入:head=[1,2], n=2(倒数第2个是头节点1)
流程:dummy->1->2 → fast先走2步到2 → 循环终止 → slow=dummy → 删除1,结果为 [2]。
边界3:删除链表尾节点
输入:head=[1,2,3], n=1
流程:fast先走1步到1 → 同步遍历到fast=3、slow=2 → 删除尾节点3,结果为 [1,2]。
六、算法复杂度分析
- 时间复杂度 O(L) :L 为链表节点总数,整段代码仅对链表进行一次遍历,无重复遍历,效率极高。
- 空间复杂度 O(1) :仅创建了 1 个虚拟节点、2 个指针变量,使用常数级额外空间,无额外容器、递归栈消耗。
七、常见易错点总结
- 不使用虚拟节点:会导致删除头节点时无前置节点,代码需要大量边界判断,冗余且容易出错。
- 循环条件写错 :若写
while(fast),快指针会走到 null,导致慢指针停在待删除节点本身,无法完成删除。 - 返回原 head :头节点被删除时,原 head 失效,必须返回
dummy.next。 - n 步遍历逻辑错误:必须快指针先行、再同步移动,顺序颠倒会导致定位错误。
八、解题总结
本题的核心精髓是 虚拟节点统一边界 + 快慢指针间距定位,用一次遍历解决倒数节点查找问题,规避了传统解法的二次遍历缺陷,同时完美兼容所有极端边界场景,是链表双指针题型的经典模板,可直接复用解决同类链表定位、删除问题。