JS 链表详解:基于固定示例的单向/双向链表与中间节点删除实战
在 JavaScript 数据结构体系中,数组凭借连续内存的优势实现 O(1) 随机访问,但非首尾位置的新增/删除需移动大量元素,性能堪忧。而链表通过节点间的引用串联数据,内存布局灵活,能将新增/删除操作的时间复杂度优化到 O(1)(找到目标节点后),成为数组的重要补充。
本文将完全基于你提供的固定链表示例,从基础结构解析入手,深入讲解单向链表与双向链表的核心特性,再通过实战案例拆解中间节点的删除逻辑,帮你彻底掌握链表的核心用法。
一、先明确核心:你提供的固定链表示例
后续所有讲解和实战,均围绕以下两段固定示例代码展开。这两段代码分别定义了单向链表和双向链表的节点及关联关系,是我们分析的基础:
1.1 固定单向链表示例
// 头节点是 head
let head = { value: 1, next: undefined }
const node2 = { value: 2, next: undefined }
const node3 = { value: 3, next: undefined }
const node4 = { value: 4, next: undefined }
// 建立链表之间的关系
head.next = node2
node2.next = node3
node3.next = node4
// 最终结构:head(1) -> node2(2) -> node3(3) -> node4(4) -> undefined
1.2 固定双向链表示例
// 假设链表的头节点是 head
let head = { value: 1, next: undefined, prev: undefined }
const node2 = { value: 2, next: undefined, prev: undefined }
const node3 = { value: 3, next: undefined, prev: undefined }
const node4 = { value: 4, next: undefined, prev: undefined }
// 建立链表之间的关系
head.next = node2
// node2 的上一个节点指向 head
node2.prev = head
// node2 的下一个节点指向 node3
node2.next = node3
// node3 的上一个节点指向 node2
node3.prev = node2
// node3 的下一个节点指向 node4
node3.next = node4
// node4 的上一个节点指向 node3
node4.prev = node3
// 最终结构:head(1) <-> node2(2) <-> node3(3) <-> node4(4)
二、链表基础:从固定示例看核心结构
从你提供的示例中,我们能清晰提炼出链表的核心组成——节点,以及两种链表的核心差异——指针域(引用域)的数量。
2.1 节点的通用结构
无论是单向还是双向链表,每个节点都包含数据域和指针域:
- 数据域(value):存储具体的数据(示例中为 1、2、3、4);
- 指针域(next/prev):存储相邻节点的引用,用于串联节点(单向链表只有 next,双向链表新增 prev)。
2.2 单向链表:仅支持“单向遍历”
从固定示例可见,单向链表的每个节点仅包含 next 指针,用于指向后继节点(下一个节点):
- 遍历方向:只能从 head 开始,顺着 next 指针向后遍历(head → node2 → node3 → node4);
- 局限性:无法回溯(比如从 node3 无法直接找到 node2),这会影响中间节点的删除效率。
2.3 双向链表:支持“双向遍历”
双向链表在单向链表的基础上,给每个节点新增了 prev 指针,用于指向前驱节点(上一个节点):
- 遍历方向:双向可选(head → node2 → node3 → node4 或 node4 → node3 → node2 → head);
- 优势:可直接通过节点的 prev 指针回溯到前驱节点,无需额外记录,极大优化了中间节点的操作效率。
2.4 链表 vs 数组:核心差异对比
结合固定示例,我们可以更直观地对比链表与数组的核心差异:
| 数据结构 | 内存布局 | 访问方式 | 新增/删除(中间位置) |
|---|---|---|---|
| 数组 | 连续内存 | 索引访问(O(1)) | 需移动元素(O(n)) |
| 链表(单向/双向) | 非连续内存(节点分散) | 遍历访问(O(n)) | 仅改指针(O(1),找到节点后) |
三、实战重点:基于固定示例删除中间节点
链表操作的核心是“修改指针指向”,而中间节点的删除是高频考点。结合你提供的固定示例(中间节点为 node3,value=3),我们分别实现单向链表和双向链表的中间节点删除,并对比差异。
说明:所有案例均基于你给出的固定节点(head、node2、node3、node4)及关联关系,不额外新增或修改节点定义。
3.1 单向链表:删除中间节点(node3)
单向链表的痛点:无法通过 node3 直接找到其前驱节点 node2,因此必须先遍历链表,找到 node3 的同时,记录其前驱节点。
删除逻辑:找到 node3 后,将其前驱节点(node2)的 next 指针,直接指向 node3 的后继节点(node4),跳过 node3,完成删除。
// 1. 你的固定单向链表示例(直接复用)
let head = { value: 1, next: undefined }
const node2 = { value: 2, next: undefined }
const node3 = { value: 3, next: undefined }
const node4 = { value: 4, next: undefined }
head.next = node2
node2.next = node3
node3.next = node4
// 2. 删除中间节点 node3(value=3)
function deleteSinglyMiddleNode(head, targetValue) {
// 边界判断:空链表或无中间节点(仅1个节点)
if (!head || !head.next) return head
let prev = null // 记录前驱节点
let current = head // 当前遍历节点
// 遍历查找目标节点(node3,value=3),并记录前驱
while (current) {
if (current.value === targetValue) {
// 核心:让前驱节点的 next 指向当前节点的 next(跳过当前节点)
prev.next = current.next
break // 找到并删除,退出遍历
}
prev = current // 前驱节点后移
current = current.next // 当前节点后移
}
return head
}
// 执行删除
deleteSinglyMiddleNode(head, 3)
// 验证结果:遍历链表
function traverseSingly(head) {
const result = []
let current = head
while (current) {
result.push(current.value)
current = current.next
}
console.log("单向链表删除中间节点后:", result) // 输出:[1, 2, 4]
}
traverseSingly(head)
关键说明:单向链表删除中间节点的核心是“遍历+记录前驱”,因为无法通过中间节点回溯,必须在遍历过程中同步保存前驱节点的引用。
3.2 双向链表:删除中间节点(node3)
双向链表的优势:node3 自带 prev 指针,可直接指向其前驱节点 node2,无需遍历记录,删除逻辑更简洁。
删除逻辑:同时修改 node3 前驱节点(node2)的 next 指针,和 node3 后继节点(node4)的 prev 指针,双向跳过 node3。
// 1. 你的固定双向链表示例(直接复用)
let head = { value: 1, next: undefined, prev: undefined }
const node2 = { value: 2, next: undefined, prev: undefined }
const node3 = { value: 3, next: undefined, prev: undefined }
const node4 = { value: 4, next: undefined, prev: undefined }
head.next = node2
node2.prev = head
node2.next = node3
node3.prev = node2
node3.next = node4
node4.prev = node3
// 2. 删除中间节点 node3(value=3)
function deleteDoublyMiddleNode(targetNode) {
// 目标节点的前驱节点(直接通过 prev 获取,无需遍历)
const prevNode = targetNode.prev
// 目标节点的后继节点(直接通过 next 获取)
const nextNode = targetNode.next
// 核心:双向指针重定向,跳过目标节点
prevNode.next = nextNode // 前驱的 next 指向后继
nextNode.prev = prevNode // 后继的 prev 指向前驱
}
// 执行删除:直接传入中间节点 node3(也可通过遍历获取,此处简化)
deleteDoublyMiddleNode(node3)
// 验证结果:遍历链表
function traverseDoubly(head) {
const result = []
let current = head
while (current) {
result.push(current.value)
current = current.next
}
console.log("双向链表删除中间节点后:", result) // 输出:[1, 2, 4]
}
traverseDoubly(head)
关键说明:双向链表凭借 prev 指针,无需遍历即可直接获取前驱节点,删除逻辑更高效、简洁。如果不知道目标节点的引用,也可通过遍历找到 node3 后执行删除,同样比单向链表少一步“记录前驱”的操作。
四、两种链表的核心差异与适用场景
基于固定示例的操作对比,我们能清晰总结出单向链表和双向链表的差异及适用场景:
| 链表类型 | 核心优势 | 核心劣势 | 适用场景 |
|---|---|---|---|
| 单向链表 | 结构简单,占用内存少(仅需一个 next 指针) | 无法回溯,中间节点操作需遍历记录前驱 | 元素操作以“尾增/头删”为主,无需回溯的场景(如简单队列实现) |
| 双向链表 | 支持双向遍历,中间节点操作高效(无需记录前驱) | 结构复杂,占用内存多(多一个 prev 指针) | 需要频繁对中间节点进行新增/删除,或需要回溯遍历的场景(如浏览器前进后退、LRU 缓存) |
五、总结
通过你提供的固定链表示例,我们清晰掌握了链表的核心逻辑:
- 链表的本质是“节点通过指针串联”,内存非连续,核心优势是新增/删除高效;
- 单向链表仅含 next 指针,无法回溯,中间节点删除需记录前驱;
- 双向链表新增 prev 指针,支持回溯,中间节点删除更简洁,但占用更多内存。