一、什么是链表?
链表是一种常见的数据结构,与数组同为线性表,但存储方式截然不同。
-
数组:有序、线性、连续型存储空间,访问效率高(O(1)),但增删元素可能需要移动大量元素(O(n))
-
链表:有序、线性、离散型存储空间,增删元素效率高(O(1)),但访问元素需要从头遍历(O(n))
注意:JavaScript 中的数组未必是真正的数组,当存储不同类型数据时可能变为哈希表结构
二、链表的基本结构
1. 链表节点的构成
每个链表节点包含两部分:
-
值域(val):存储数据 -
指针域(next):指向后续节点
// 简单对象表示
let list = {
val: 1, // 值域
next: { // 指针域
val: 2,
next: null // 尾节点指针为null
}
}
2. 用构造函数创建节点
更规范的做法是使用构造函数创建节点:
function ListNode(val) {
this.val = val;
this.next = null;
}
// 创建节点
const node1 = new ListNode(1);
const node2 = new ListNode(2);
// 连接节点
node1.next = node2; // 1 -> 2 -> null
三、链表的基本操作
1. 遍历链表
// 假设我们有一个链表: 1 -> 2 -> 3 -> ... -> 9 -> null
let list = {
val: 1,
next: {
val: 2,
next: {
val: 3,
next: {
val: 4,
next: {
val: 5,
next: {
val: 6,
next: {
val: 7,
next: {
val: 8,
next: {
val: 9,
next: null
}
}
}
}
}
}
}
}
};
// 遍历到第8个节点
let node = list;
for (let i = 1; i < 8 && node; i++) {
node = node.next;
}
console.log(node); // 输出第8个节点: {val: 8, next: {val: 9, next: null}}
2. 插入节点
// 在node1和node2之间插入node3
const node1 = new ListNode(1);
node1.next = new ListNode(2);
const node3 = new ListNode(3);
node3.next = node1.next; // 先让node3指向node2
node1.next = node3; // 再让node1指向node3
// 结果: 1 -> 3 -> 2 -> null
四、实战:合并两个有序链表
LeetCode 第21题:将两个升序链表合并为一个新的升序链表并返回。
var mergeTwoLists = function (list1, list2) {
// 创建虚拟头节点,简化边界处理
const head = new ListNode();
let cur = head;
// 遍历两个链表,比较节点值大小
while (list1 && list2) {
if (list1.val <= list2.val) {
cur.next = list1;
list1 = list1.next;
} else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
// 处理剩余节点
cur.next = list1 === null ? list2 : list1;
// 返回真正的头节点(跳过虚拟头节点)
return head.next;
};
LeetCode 第83题: 给定一个已排序的链表的头 head, 删除所有重复的元素,使每个元素只出现一次。返回 已排序的链表 。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function(head) {
var cur = head;
while(cur && cur.next) {
if(cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
};
五、总结
- 链表适合频繁增删的场景,数组适合频繁访问的场景
- 操作链表时要注意指针的正确指向,避免断链
- 虚拟头节点技巧可以简化边界条件处理