还在被数据结构"链表"折磨得头疼不已?还在为LeetCode上的链表题抓耳挠腮?别担心!今天我就带你用最通俗易懂的方式,从零开始彻底理解链表,让你不再害怕这个看似复杂的数据结构!
一、什么是链表?为什么需要它?
假设你和朋友们排队买票。如果是"普通队列",有人离开就得所有人往前挪。但如果每个人只记住前面那个人是谁,有人离开只需要改变记忆就行了,不用挪动。这就是链表的核心思想。
数组 vs 链表
数组就像一排整齐的储物柜:
- 存储连续,可以直接通过下标访问
- 查找速度快(O(1)时间复杂度)
- 但插入、删除元素需要移动大量数据
而链表则像一条"信息传递链":
- 存储离散,通过"指针"连接各个节点
- 插入、删除操作只需修改指针,效率高(O(1)时间复杂度)
- 但查找必须从头遍历,效率较低(O(n)时间复杂度)
二、链表的基本结构
链表由一个个"节点"组成,每个节点包含两部分:
- 数据域:存储实际的数据
- 指针域:指向下一个节点的地址
用JavaScript代码表示一个简单的链表节点:
function ListNode(val, next) {
this.val = val // 数据域
this.next = next ? next : null // 指针域
}
一个完整的链表结构:
const list = { // 链表的头节点
val: 'a',
next: {
val: 'b',
next: {
val: 'c',
next: null // 链表的尾节点指向null
}
}
}
三、链表的基本操作
1. 创建链表
创建一个简单的链表:
const head = new ListNode('a');
const nodeB = new ListNode('b');
const nodeC = new ListNode('c');
head.next = nodeB;
nodeB.next = nodeC;
2. 遍历链表
let current = head;
while (current !== null) {
console.log(current.val); // 输出当前节点的值
current = current.next; // 移动到下一个节点
}
3. 插入节点
在位置B和C之间插入新节点X:
const nodeX = new ListNode('x');
nodeX.next = nodeB.next; // 先连后面
nodeB.next = nodeX; // 再连前面
关键点:先连后面,再连前面,避免丢失后续链表。
4. 删除节点
删除节点C:
nodeB.next = nodeC.next; // 直接"跳过"nodeC
四、LeetCode实战:解决经典链表问题
问题1:合并两个有序链表 (LeetCode 21)
给你两个升序链表,请合并成一个新的升序链表。
var mergeTwoLists = function(list1, list2) {
let dummy = new ListNode(); // 创建一个哑节点作为新链表的起点
let current = dummy; // 用current遍历并构建新链表
// 当两个链表都不为空时循环比较
while (list1 !== null && list2 !== null) {
// 取较小值的节点加入新链表
if (list1.val < list2.val) {
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
// 处理剩余部分
if (list1 !== null) {
current.next = list1;
} else {
current.next = list2;
}
return dummy.next; // 返回哑节点的下一个节点
};
这题的关键是理解"哑节点"技巧——用一个临时节点作为新链表的起点,避免处理头节点的特殊情况。
问题2:删除排序链表中的重复元素 (LeetCode 83/136)
删除链表中的重复元素,使每个元素只出现一次。
var deleteDuplicates = function(head) {
let current = head;
while (current && current.next) {
if (current.val === current.next.val) {
// 遇到重复值,直接跳过下一节点
current.next = current.next.next;
} else {
// 没有重复,继续前进
current = current.next;
}
}
return head;
};
问题3:删除排序链表中的所有重复元素 (LeetCode 82)
删除链表中所有出现重复的元素,只保留原始链表中没有重复出现的数字。
var deleteDuplicates = function(head) {
if (!head) return head;
// 创建哑节点,处理头节点可能被删除的情况
let dummy = new ListNode(0, head);
let prev = dummy;
while (prev.next && prev.next.next) {
if (prev.next.val === prev.next.next.val) {
// 遇到重复值
let val = prev.next.val;
// 跳过所有重复值
while (prev.next && prev.next.val === val) {
prev.next = prev.next.next;
}
} else {
// 没有重复,前进一步
prev = prev.next;
}
}
return dummy.next;
};
五、链表的进阶操作
除了基本的单向链表,还有:
- 双向链表:每个节点有两个指针,分别指向前后节点
- 循环链表:尾节点指向头节点,形成一个环
- 跳表:在链表基础上添加多级索引,提高查找效率
六、链表的实际应用
链表在实际开发中广泛应用:
- 浏览器的前进/后退功能:双向链表的典型应用
- 操作系统中的任务调度:循环链表管理进程
- 大数据存储:当数据量不确定时,链表比数组更灵活
- LRU缓存算法:使用链表实现缓存淘汰策略
七、链表学习技巧总结
- 画图理解:链表操作最怕"断链",画图可以帮助你理清指针变化
- 掌握哑节点技巧:创建一个dummy节点简化头节点的处理
- 双指针法:解决"倒数第K个节点"、"中间节点"等问题的利器
- 递归思维:链表天然适合递归,很多复杂操作用递归更简洁
本文从初学者视角出发,深入浅出地讲解了链表的基本概念、结构特点与数组的对比,详细介绍了链表的创建、遍历、插入和删除操作,并通过LeetCode经典题目实战演练,帮助读者快速掌握链表的核心技巧,轻松应对链表相关问题。