链表基础
为什么前端开发要学习链表?
链表在前端开发中有很多实际应用:
- 实现浏览器的前进/后退功能
- 组件更新队列管理
- 事件循环任务队列
- 文件系统的目录结构
- 撤销/重做功能实现
链表 vs 数组
数组的局限性
- 固定大小,扩容成本高
- 插入和删除需要移动元素
- 连续内存空间要求高
链表的优势
- 动态大小,按需分配内存
- O(1)时间复杂度的插入和删除
- 灵活的内存使用
链表实现
1. 节点定义
js
class ListNode {
constructor(val) {
this.val = val; // 节点值
this.next = null; // 指向下一个节点的指针
}
}2. 链表类实现
js
class LinkedList {
constructor() {
this.head = null; // 头节点
this.size = 0; // 链表大小
}
// 在头部添加节点
addAtHead(val) {
const newNode = new ListNode(val);
newNode.next = this.head;
this.head = newNode;
this.size++;
}
// 在尾部添加节点
addAtTail(val) {
const newNode = new ListNode(val);
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
this.size++;
}
// 在指定位置添加节点
addAtIndex(index, val) {
if (index < 0 || index > this.size) return;
if (index === 0) {
this.addAtHead(val);
return;
}
const newNode = new ListNode(val);
let current = this.head;
for (let i = 0; i < index - 1; i++) {
current = current.next;
}
newNode.next = current.next;
current.next = newNode;
this.size++;
}
// 删除指定位置的节点
deleteAtIndex(index) {
if (index < 0 || index >= this.size) return;
if (index === 0) {
this.head = this.head.next;
this.size--;
return;
}
let current = this.head;
for (let i = 0; i < index - 1; i++) {
current = current.next;
}
current.next = current.next.next;
this.size--;
}
// 获取指定位置的节点值
get(index) {
if (index < 0 || index >= this.size) return -1;
let current = this.head;
for (let i = 0; i < index; i++) {
current = current.next;
}
return current.val;
}
}实际应用示例
1. 实现简单的撤销/重做功能
js
class UndoRedoManager {
constructor() {
this.undoStack = new LinkedList();
this.redoStack = new LinkedList();
}
// 执行操作
execute(action) {
this.undoStack.addAtHead(action);
this.redoStack = new LinkedList(); // 清空重做栈
}
// 撤销操作
undo() {
if (this.undoStack.size === 0) return null;
const action = this.undoStack.get(0);
this.undoStack.deleteAtIndex(0);
this.redoStack.addAtHead(action);
return action;
}
// 重做操作
redo() {
if (this.redoStack.size === 0) return null;
const action = this.redoStack.get(0);
this.redoStack.deleteAtIndex(0);
this.undoStack.addAtHead(action);
return action;
}
}2. 实现LRU缓存
js
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
this.list = new LinkedList();
}
get(key) {
if (!this.cache.has(key)) return -1;
// 将访问的节点移到头部
const value = this.cache.get(key);
this.list.deleteAtIndex(this.list.indexOf(key));
this.list.addAtHead(key);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.list.deleteAtIndex(this.list.indexOf(key));
} else if (this.cache.size >= this.capacity) {
const lastKey = this.list.get(this.list.size - 1);
this.cache.delete(lastKey);
this.list.deleteAtIndex(this.list.size - 1);
}
this.cache.set(key, value);
this.list.addAtHead(key);
}
}常见链表操作技巧
1. 双指针技巧
js
// 检测环形链表
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;
}2. 反转链表
js
function reverseList(head) {
let prev = null;
let current = head;
while (current) {
const next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
}性能优化建议
- 使用尾指针优化尾部操作
- 适当使用双向链表
- 注意内存泄漏问题
- 合理使用虚拟头节点
常见面试题
- 如何检测链表中的环?
- 如何找到链表的中间节点?
- 如何合并两个有序链表?
推荐练习
- LeetCode 206: 反转链表
- LeetCode 141: 环形链表
- LeetCode 21: 合并两个有序链表