19.LeetCode 删除链表的倒数第 N 个节点 超详细题解(JS 版)

一、题目概述

题目要求 :给定一个单向链表的头节点 head,删除链表的倒数第 n 个节点,并返回删除后的链表头节点。

解题难点

  • 单向链表无法逆向遍历,不能直接从末尾往前找节点;
  • 常规解法需要先遍历一次统计链表长度,再二次遍历删除节点,效率较低;
  • 存在特殊边界:当需要删除链表第一个节点时,直接操作头节点会出现空指针、节点丢失问题。

最优解题思路虚拟头节点 + 快慢指针(双指针)一次遍历法,仅遍历链表一次即可完成删除,时间复杂度最优。

二、核心算法原理

1. 虚拟头节点(dummy)作用

这是解决本题边界问题的关键。我们手动在链表最前端新增一个值为 0 的虚拟节点,让虚拟节点的后继指向原链表头节点。

核心价值:统一所有节点的删除逻辑。无论删除的是链表头节点、中间节点、尾节点,待删除节点都存在前驱节点,彻底避免了删除头节点时无前置节点的特殊情况,无需单独写边界判断代码。

2. 快慢指针核心逻辑

通过两个指针的步数差,精准定位倒数第 n 个节点的前驱节点:

  1. 快指针、慢指针初始均指向虚拟头节点;
  2. 快指针先单独向后移动 n 步,让快慢指针之间形成 n 个节点的固定间距
  3. 快慢指针同时向后同步移动,直到快指针走到链表最后一个节点;
  4. 此时慢指针精准停在倒数第 n 个节点的前一个节点
  5. 修改慢指针的后继指针,跳过待删除节点,完成删除操作。

三、完整代码逐行精讲

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)

  1. 构建虚拟节点:dummy(0) -> 1 -> 2 -> 3 -> 4 -> 5

  2. 初始指针:slow = dummyfast = dummy

  3. 快指针先走 2 步:fast 依次走到 1、2,此时快慢指针间距为 2

  4. 同步遍历:

    1. fast=2、slow=0 → fast=3、slow=1
    2. fast=3、slow=1 → fast=4、slow=2
    3. fast=4、slow=2 → fast=5、slow=3
  5. 此时 fast.next = null,循环终止,slow 停在节点 3(待删除节点4的前驱)

  6. 执行删除:3.next = 4.next,链表变为 0->1->2->3->5

  7. 返回 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 个指针变量,使用常数级额外空间,无额外容器、递归栈消耗。

七、常见易错点总结

  1. 不使用虚拟节点:会导致删除头节点时无前置节点,代码需要大量边界判断,冗余且容易出错。
  2. 循环条件写错 :若写 while(fast),快指针会走到 null,导致慢指针停在待删除节点本身,无法完成删除。
  3. 返回原 head :头节点被删除时,原 head 失效,必须返回dummy.next
  4. n 步遍历逻辑错误:必须快指针先行、再同步移动,顺序颠倒会导致定位错误。

八、解题总结

本题的核心精髓是 虚拟节点统一边界 + 快慢指针间距定位,用一次遍历解决倒数节点查找问题,规避了传统解法的二次遍历缺陷,同时完美兼容所有极端边界场景,是链表双指针题型的经典模板,可直接复用解决同类链表定位、删除问题。

相关推荐
kisshyshy1 小时前
前端路由进化史:从刷新白屏到SPA,手写一个Hash路由就懂了!
前端·javascript·react.js
java1234_小锋2 小时前
Vue3专题 - 条件渲染
前端·javascript·vue.js
嘟嘟07172 小时前
从零理解 useRef:React 中操作 DOM 与持久化变量的唯一桥梁
javascript
名字还没想好☜2 小时前
React 用 Portal + Context 实现全局 Toast:一次调用、自动消失与队列管理
前端·javascript·react.js·react·next.js
嘟嘟07172 小时前
摘要 手把手实现路由鉴权守卫,逐行拆解 ProtectRoute、localStorage、Navigate state、useLocation、FormDat
javascript
肉肉不吃 肉3 小时前
无渲染组件和组合式函数
前端·javascript·vue.js
其美杰布-富贵-李3 小时前
01 从项目结构开始认识 Vue 3
javascript·vue.js·ecmascript
sugar__salt3 小时前
前端路由技术详解:从传统多页到 React SPA 的完整进化之路
前端·javascript·react.js·前端框架
用户84298142418103 小时前
JS代码压缩实测:可减小体积、提高执行效率!
前端·javascript·后端