链表是前端算法面试中的高频考点,尽管 JavaScript 没有原生的链表数据结构,但理解链表的核心逻辑、掌握其经典算法,不仅能应对面试,还能加深对 “引用”“指针” 等底层概念的理解。本文结合删除节点、链表反转、判断环、删除倒数第 N 个节点等经典场景,梳理链表算法的核心技巧与实现思路。
一、链表基础认知
1.1 链表的本质
链表是一种线性数据结构,由若干个节点组成,每个节点包含两部分:
val:节点存储的数值;next:指向下一个节点的引用(指针)。
单链表的最后一个节点的next为null,整个链表通过next指针串联。在 JavaScript 中,我们通常用构造函数模拟链表节点:
// 定义单链表节点
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
与数组相比,链表的优势是插入 / 删除操作无需移动大量元素(仅需修改指针),劣势是无法随机访问(必须从表头遍历到目标节点)。前端领域中,React Fiber 架构的底层就用到了链表结构,可见其重要性。
1.2 链表算法的核心痛点
链表操作的难点在于边界条件处理:
- 表头节点没有前驱节点,删除 / 修改表头时需特殊处理;
- 表尾节点的
next为null,遍历到表尾时易出现空指针异常; - 空链表、单节点链表等极端场景容易遗漏。
解决这些问题的核心技巧是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;
}
该实现的问题:
- 若链表为空(
head为null),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始终指向新的表头,无需单独处理; - 即使链表为空(
head为null),dummy仍存在,不会触发空指针异常; - 所有节点的删除逻辑完全统一(都是修改前驱节点的
next)。
三、核心技巧 2:快慢指针 —— 解决位置与环的问题
快慢指针是指两个同方向遍历的指针,快指针的步长大于慢指针(通常为 2:1),利用 “速度差” 解决 “找倒数第 N 个节点”“判断链表是否有环” 等问题,实现一次遍历完成目标,提升效率。
3.1 实战 1:判断链表是否有环
链表有环的本质是:某个节点的next指向链表中已存在的节点,导致遍历无法终止。快慢指针的核心逻辑是:
- 若无环:快指针会先到达表尾(
fast或fast.next为null); - 若有环:快指针会绕环追上慢指针(快慢指针指向同一节点)。
实现代码:
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.next为null),慢指针恰好指向 “倒数第 n 个节点的前驱”; - 直接修改慢指针的
next,即可删除目标节点; - Dummy 节点确保:即使 n 等于链表长度(删除原表头),
slow.next仍有值,不会报错。
四、经典算法:链表反转(Dummy + 头插法)
链表反转是面试必考题,最易理解的实现方式是Dummy 节点 + 头插法:将原链表的每个节点依次 “插入” 到 Dummy 节点的后面,最终 Dummy 的next就是反转后的表头。
4.1 头插法核心三步
- 保存当前节点的下一个节点(避免丢失后续链表);
- 当前节点的
next指向 Dummy 的next(连接已反转的部分); - 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 = null,cur = 1; - 第一步:
next = 2,1.next = null,dummy.next = 1→dummy->1->null; - 第二步:
cur = 2,next = 3,2.next = 1,dummy.next = 2→dummy->2->1->null; - 第三步:
cur = 3,next = null,3.next = 2,dummy.next = 3→dummy->3->2->1->null; - 循环结束,返回
dummy.next即3,完成反转。
五、学习总结与建议
5.1 核心思想
- 指针是核心:链表算法的本质是 “指针的移动与修改”,手动模拟每一步指针的指向变化,是理解算法的关键;
- Dummy 节点是万能钥匙:几乎所有链表边界问题(表头删除、空链表)都能通过 Dummy 节点解决,建议优先使用;
- 快慢指针提效:涉及 “倒数第 N 个”“环检测” 的场景,快慢指针可将时间复杂度从 O (2n) 降至 O (n)。
5.2 练习建议
- 基础巩固:手写 ListNode 构造函数,模拟 “创建链表”“遍历链表” 等基础操作;
- 边界测试:针对 “空链表”“单节点链表”“删除表头 / 表尾” 等场景测试代码;
- 变种拓展:在基础算法上拓展(如删除所有指定值的节点、反转部分链表、找链表的中间节点);
- 手写复盘:脱离编辑器,手写核心算法,强化指针逻辑的记忆。
链表算法看似复杂,实则规律可循。掌握 Dummy 节点和快慢指针两大技巧,拆解每一步指针的移动逻辑,多模拟、多练习,就能攻克绝大多数链表面试题。