🌱 从“串珠子”到“指针舞蹈”:用 JavaScript 彻底吃透链表核心操作
你不需要懂 C 语言或内存地址,只要会用 JavaScript 的对象,就能彻底掌握链表
在学习 JavaScript 的过程中,很多人都会在某一刻被“链表”卡住。
它不像数组那样直观,也没有对象那样自由,却频繁出现在面试和算法题中。
其实,链表并不难,只是我们还没找到合适的理解方式。
本文将通过几个最经典的链表操作——删除节点、反转链表、判断是否有环、删除倒数第 N 个节点,一步步带你建立起对链表的“感觉”。
一、链表到底是什么?先换个角度看问题
如果把数组想象成一排连续的房子:
[ A ][ B ][ C ][ D ]
那链表更像是一串用绳子串起来的珠子:
A -> B -> C -> D -> null
每一颗珠子(节点)只关心两件事:
{
val: 当前节点存的数据,
next: 指向下一个节点的引用
}
📌 链表与数组的关键区别
| 对比 | 数组 | 链表 |
|---|---|---|
| 内存 | 连续 | 不连续 |
| 访问 | 下标直接访问 | 只能一个个找 |
| 插入/删除 | 麻烦(要挪) | 方便(改指针) |
链表的所有“魔法”,本质上就一句话:
不断地改 next 指向
二、删除链表中的节点:为什么头节点最麻烦?
1️⃣ 基础版:直接遍历删除
先来看一个最直观的实现:
function remove(head, val) {
if (head && 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.next -
否则:
- 从头开始遍历
- 找到目标节点的前一个
- 让它的
next指向下下个节点
📌 生活比喻
像一列火车:
车头 -> 车厢1 -> 车厢2 -> 车厢3
你不能直接“拽走中间的车厢”,
只能让 前一节车厢改挂钩方向。
❗问题来了:为什么头节点要特殊处理?
因为:
- 头节点没有前驱
- 而删除操作,偏偏需要“前一个节点”
于是我们就写了一个 if (head.val === val) 的特判。
特判一多,代码就开始变得别扭。
三、dummy 节点:链表世界的“垫脚石”
为了解决“头节点没有前驱”的问题,引入了一个非常经典的设计:
dummy 节点(哨兵节点)
1️⃣ dummy 是什么?
- 一个假的节点
- 不存真实数据
- 永远站在真正头节点的前面
dummy -> head -> ...
就像:
在队伍最前面安排一个“空气人”,
让每个人前面都有人
2️⃣ 使用 dummy 重写删除逻辑
function remove(head, val) {
const dummy = new ListNode(0)
dummy.next = head
let cur = dummy
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next
break
}
cur = cur.next
}
return dummy.next
}
✨ 优点一眼可见
- 不用再区分是不是头节点
- 所有删除逻辑 完全统一
- 思维成本明显降低
📌 一句话总结
dummy 节点不是为了存数据,
而是为了消灭边界条件
四、链表反转:头插法其实一点都不神秘
链表反转是很多人的“心理阴影”,
但一旦理解了,它甚至比数组反转更优雅。
1️⃣ 思路先行:别急着看代码
反转前:
1 -> 2 -> 3 -> null
反转后:
3 -> 2 -> 1 -> null
关键问题只有一个:
当前节点,应该指向谁?
答案是:
指向「已经反转好的那一部分的头」
2️⃣ dummy + 头插法实现反转
function reverseList(head) {
const dummy = new ListNode(0)
let cur = head
while (cur) {
const next = cur.next
cur.next = dummy.next
dummy.next = cur
cur = next
}
return dummy.next
}
3️⃣ 拆解成“三步舞蹈”
每一轮循环,都只做三件事:
1️⃣ 保存下一个节点
const next = cur.next
👉 不保存就会“断链”
2️⃣ 当前节点插到反转区最前面
cur.next = dummy.next
dummy.next = cur
👉 这一步就是“头插法”
3️⃣ 指针前进
cur = next
五、快慢指针:两个人走路,为什么能发现环?
1️⃣ 什么是链表中的“环”?
1 -> 2 -> 3 -> 4
↑ ↓
← ← ← ←
节点指回前面的节点,就形成了环。
2️⃣ 快慢指针的直觉理解
- 慢指针:一步一步走
- 快指针:一次走两步
如果:
- 没有环 👉 快指针先到
null - 有环 👉 快指针一定会追上慢指针
3️⃣ 判断链表是否有环
function hasCycle(head) {
let slow = head
let fast = head
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
if (slow === fast) {
return true
}
}
return false
}
📌 为什么一定能追上?
就像操场跑步:
- 一个人快
- 一个人慢
- 在一个封闭跑道上
👉 迟早会相遇
六、删除倒数第 N 个节点:快慢指针的进阶用法
来自19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
1️⃣ 为什么“倒数”很难?
因为链表:
- 不能反向遍历
- 不知道长度,就没法直接定位
2️⃣ 一个非常巧妙的解法
让快指针先走 N 步
当快指针到终点时:
- 慢指针正好停在
👉 倒数第 N 个节点的前一个
3️⃣ 完整实现
const removeNthFromEnd = function(head, n) {
const dummy = new ListNode(0)
dummy.next = head
let fast = dummy
let slow = dummy
for (let i = 0; i < n; i++) {
fast = fast.next
}
while (fast.next) {
fast = fast.next
slow = slow.next
}
slow.next = slow.next.next
return dummy.next
}
🧠 核心理解
- 快慢指针之间始终保持 N 步距离
- 快指针到头,慢指针刚好对位
📌 像两个人拿着尺子一起走路
七、链表的三大“万能套路”
学完这些例子,其实可以总结出链表题的固定解题模型:
✅ 1️⃣ dummy 节点
- 删除
- 反转
- 倒数节点
👉 统一头节点逻辑
✅ 2️⃣ 快慢指针
- 是否有环
- 倒数第 N 个
- 中间节点
👉 用时间换空间
✅ 3️⃣ 指针操作的本质
链表题 ≠ 写代码
链表题 = 想清楚 next 指向谁
八、结语:链表并不复杂,只是需要“耐心感”
如果说数组像记事本,
那链表更像是一串钥匙扣。
你不能跳着找,
但你可以随时加、随时拆。
当你真正理解了:
- dummy 的意义
- 快慢指针的节奏
- 每一次
cur.next = ...背后的含义
链表,就会从“噩梦”变成“模板题”。
🌟 真正的进步,不是记住代码,而是看懂变化。