JS 链表算法核心技巧学习笔记

45 阅读7分钟

链表是前端算法面试中的高频考点,尽管 JavaScript 没有原生的链表数据结构,但理解链表的核心逻辑、掌握其经典算法,不仅能应对面试,还能加深对 “引用”“指针” 等底层概念的理解。本文结合删除节点、链表反转、判断环、删除倒数第 N 个节点等经典场景,梳理链表算法的核心技巧与实现思路。

一、链表基础认知

1.1 链表的本质

链表是一种线性数据结构,由若干个节点组成,每个节点包含两部分:

  • val:节点存储的数值;
  • next:指向下一个节点的引用(指针)。

单链表的最后一个节点的nextnull,整个链表通过next指针串联。在 JavaScript 中,我们通常用构造函数模拟链表节点:

// 定义单链表节点
function ListNode(val, next) {
    this.val = (val === undefined ? 0 : val);
    this.next = (next === undefined ? null : next);
}

与数组相比,链表的优势是插入 / 删除操作无需移动大量元素(仅需修改指针),劣势是无法随机访问(必须从表头遍历到目标节点)。前端领域中,React Fiber 架构的底层就用到了链表结构,可见其重要性。

1.2 链表算法的核心痛点

链表操作的难点在于边界条件处理

  • 表头节点没有前驱节点,删除 / 修改表头时需特殊处理;
  • 表尾节点的nextnull,遍历到表尾时易出现空指针异常;
  • 空链表、单节点链表等极端场景容易遗漏。

解决这些问题的核心技巧是dummy 哨兵节点快慢指针,下面逐一拆解。

二、核心技巧 1:Dummy 哨兵节点 —— 简化边界处理

2.1 什么是 Dummy 节点

Dummy 节点(哨兵节点)是人为添加的 “伪节点”,不存储真实数据,通常作为链表的 “伪表头”(dummy.next指向真实表头)。其核心作用是统一所有节点的处理逻辑,让表头节点拥有和普通节点一样的前驱节点,彻底解决边界问题。

2.2 实战:删除链表中第一个指定值的节点

无 Dummy 节点的实现(存在边界缺陷)

function remove(head, val) {
    // 特殊处理:表头节点就是目标节点
    if (head.val === val) {
        return head.next;
    }
    let cur = head;
    // 遍历查找目标节点的前驱
    while (cur.next) {
        if (cur.next.val === val) {
            cur.next = cur.next.next;
            break;
        }
        cur = cur.next;
    }
    return head;
}

该实现的问题:

  • 若链表为空(headnull),head.val会直接报错;
  • 若表头节点是目标节点,需单独分支处理,逻辑不统一;
  • 若链表中无目标节点,遍历结束后无额外处理(虽不影响,但逻辑不够优雅)。

有 Dummy 节点的实现(优雅解决边界)

function remove(head, val) {
    // 创建Dummy节点,伪表头
    const dummy = new ListNode(0);
    dummy.next = head; // 指向真实表头
    
    let cur = dummy; // 从Dummy节点开始遍历
    while (cur.next) {
        // 找到目标节点,直接修改指针删除
        if (cur.next.val === val) {
            cur.next = cur.next.next;
            break;
        }
        cur = cur.next;
    }
    // 最终返回真实表头(避免删除原表头后返回错误)
    return dummy.next;
}

核心优势:

  • 无论是否删除表头,dummy.next始终指向新的表头,无需单独处理;
  • 即使链表为空(headnull),dummy仍存在,不会触发空指针异常;
  • 所有节点的删除逻辑完全统一(都是修改前驱节点的next)。

三、核心技巧 2:快慢指针 —— 解决位置与环的问题

快慢指针是指两个同方向遍历的指针,快指针的步长大于慢指针(通常为 2:1),利用 “速度差” 解决 “找倒数第 N 个节点”“判断链表是否有环” 等问题,实现一次遍历完成目标,提升效率。

3.1 实战 1:判断链表是否有环

链表有环的本质是:某个节点的next指向链表中已存在的节点,导致遍历无法终止。快慢指针的核心逻辑是:

  • 若无环:快指针会先到达表尾(fastfast.nextnull);
  • 若有环:快指针会绕环追上慢指针(快慢指针指向同一节点)。

实现代码:

function hasCycle(head) {
    // 空链表或单节点链表无环
    if (!head || !head.next) return false;
    
    let slow = head; // 慢指针:每次走1步
    let fast = head; // 快指针:每次走2步
    
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
        // 快慢指针相遇,说明有环
        if (slow === fast) return true;
    }
    // 快指针到达表尾,无环
    return false;
}

关键细节:

  • 循环条件必须是fast && fast.next:避免fast.next.next触发空指针异常;
  • 无需担心 “快指针跳过慢指针”:快指针步长为 2,慢指针步长为 1,若有环,快指针最终会和慢指针相遇(类似操场跑步,快的人总会追上慢的人)。

3.2 实战 2:删除链表的倒数第 N 个节点

常规思路是先遍历链表获取长度,再遍历到 “正数第 length-N 个节点” 删除,但需要两次遍历。用快慢指针可实现一次遍历完成,结合 Dummy 节点处理边界。

实现代码:

const removeNthFromEnd = function(head, n) {
    // Dummy节点:避免删除原表头的边界问题
    const dummy = new ListNode(0);
    dummy.next = head;
    
    let fast = dummy; // 快指针
    let slow = dummy; // 慢指针
    
    // 第一步:快指针先移动n步
    for (let i = 0; i < n; i++) {
        fast = fast.next;
    }
    
    // 第二步:快慢指针同时移动,直到快指针到达表尾
    while (fast && fast.next) {
        fast = fast.next;
        slow = slow.next;
    }
    
    // 第三步:慢指针的next就是倒数第n个节点,直接删除
    slow.next = slow.next.next;
    
    // 返回新的表头
    return dummy.next;
};

逻辑拆解:

  • 快指针先移动 n 步后,快慢指针的间距为 n;
  • 快慢指针同步移动,当快指针到达表尾(fast.nextnull),慢指针恰好指向 “倒数第 n 个节点的前驱”;
  • 直接修改慢指针的next,即可删除目标节点;
  • Dummy 节点确保:即使 n 等于链表长度(删除原表头),slow.next仍有值,不会报错。

四、经典算法:链表反转(Dummy + 头插法)

链表反转是面试必考题,最易理解的实现方式是Dummy 节点 + 头插法:将原链表的每个节点依次 “插入” 到 Dummy 节点的后面,最终 Dummy 的next就是反转后的表头。

4.1 头插法核心三步

  1. 保存当前节点的下一个节点(避免丢失后续链表);
  2. 当前节点的next指向 Dummy 的next(连接已反转的部分);
  3. Dummy 的next指向当前节点(更新反转后的表头)。

4.2 实现代码

function reverseList(head) {
    // Dummy节点:始终指向已反转部分的表头
    const dummy = new ListNode(0);
    let cur = head; // 当前要处理的节点
    
    while (cur) {
        const next = cur.next; // 步骤1:保存下一个节点
        cur.next = dummy.next; // 步骤2:当前节点指向已反转的表头
        dummy.next = cur;      // 步骤3:更新反转后的表头
        cur = next;            // 处理下一个节点
    }
    
    // 返回反转后的真实表头
    return dummy.next;
}

4.3 手动模拟(理解指针移动)

以链表1->2->3->null为例:

  • 初始状态:dummy.next = nullcur = 1
  • 第一步:next = 21.next = nulldummy.next = 1 → dummy->1->null
  • 第二步:cur = 2next = 32.next = 1dummy.next = 2 → dummy->2->1->null
  • 第三步:cur = 3next = null3.next = 2dummy.next = 3 → dummy->3->2->1->null
  • 循环结束,返回dummy.next3,完成反转。

五、学习总结与建议

5.1 核心思想

  1. 指针是核心:链表算法的本质是 “指针的移动与修改”,手动模拟每一步指针的指向变化,是理解算法的关键;
  2. Dummy 节点是万能钥匙:几乎所有链表边界问题(表头删除、空链表)都能通过 Dummy 节点解决,建议优先使用;
  3. 快慢指针提效:涉及 “倒数第 N 个”“环检测” 的场景,快慢指针可将时间复杂度从 O (2n) 降至 O (n)。

5.2 练习建议

  1. 基础巩固:手写 ListNode 构造函数,模拟 “创建链表”“遍历链表” 等基础操作;
  2. 边界测试:针对 “空链表”“单节点链表”“删除表头 / 表尾” 等场景测试代码;
  3. 变种拓展:在基础算法上拓展(如删除所有指定值的节点、反转部分链表、找链表的中间节点);
  4. 手写复盘:脱离编辑器,手写核心算法,强化指针逻辑的记忆。

链表算法看似复杂,实则规律可循。掌握 Dummy 节点和快慢指针两大技巧,拆解每一步指针的移动逻辑,多模拟、多练习,就能攻克绝大多数链表面试题。