算法随笔-数据结构(链表)
本文主要介绍数据结构中的链表的特点、使用场景、ES6 实现 LinkedList 类和题解 leetCode 真题。供自己以后查漏补缺,也欢迎同道朋友交流学习。
引言
链表是一件很常见的数据结构,生活中也有很多应用场景,比如:
- 交通路线:城市中的交通路线,如公交线路,可以看作是一个链表,每个站点是一个节点,按照路线顺序连接。
- 家族家谱:从祖先到子孙的家族树也是一种链表结构,一代一代延续。
- 审批流程:我们平时请假填的请假单也是一种链表结构,每个节点代表一个审批人,按照顺序连接。
对于前端来说,学习链表这一基本数据结构,对理解更复杂的树、图、堆会更有帮助。
链表的主要特点
链表是一种基本的数据结构,具有以下主要特点:
- 灵活的结构:链表的元素可以
不连续的存储在某一个地址空间中,可以动态的增删元素,改变大小,不需要预先分配内存。 - 指针链接:每个节点通常包含两个部分:一个是存储数据的元素,另一个是指向列表中下一个节点的指针(在双向链表中,可能还包括指向前一个节点的指针)。
- 头部引用:链表通过一个称为头指针或头节点的引用开始,它指向链表的第一个节点。
- 尾部无指针:在单向链表中,最后一个节点的指针指向
null,表示链表的结束。 - 插入和删除操作:链表允许在任意位置快速插入和删除节点,只需改变相邻节点的指针即可,无需移动其他元素。
链表的应用场景
链表在计算机科学和软件开发中有多种应用场景,以下是一些常见的例子:
-
数据库索引:数据库管理系统使用链表来实现索引,如
B树和哈希表,以提高数据检索效率。 -
网络数据包处理:网络协议栈中,链表用于管理数据包的队列,特别是在
TCP/IP协议中。 -
事件处理系统:事件驱动的编程模型中,链表用于存储待处理的
事件队列。 -
游戏开发:在游戏开发中,链表可以用于管理
游戏对象,如敌人、子弹等。 -
社交网络服务:社交网络中,链表可以用于维护用户的
好友列表或社交图谱。 -
图形编辑器:在图形编辑器中,链表可以用于
管理图形元素,如节点和边。 -
文本编辑器:在文本编辑器中,链表可以用于实现
撤销/重做功能。
链表因其动态性和灵活性,在需要频繁插入和删除操作的场景中特别有用。然而,它们可能不适用于需要快速随机访问数据的情况,此时数组或其他数据结构可能是更好的选择。
JS链表简单实现
在 JS 中用next一个个链接对象,就实现了一个简单的链表:
var a = {key: 'a'}
var b = {key: 'b'}
var c = {key: 'c'}
a.next = b
b.next = c
c.next = null
// 遍历链表
var obj = a;
while(obj && obj.key) {
console.log(obj.key)
obj = obj.next
}
// 插入链表
let m = {key: 'm'}
c.next = m
m.next = d
// 删除操作
c.next = d;
// 双向链表
// 使用prev 和 next 两个指针,可以方便的实现链表的插入和删除操作
我们JS的原型链也是这种数据结构,我们可以模拟 instanceOf 这个实现:
const MyInstanceOf = function (target, obj) {
let proto = Object.getPrototypeOf(target)
while (proto) {
if (proto === obj.prototype) {
return true
}
proto = Object.getPrototypeOf(proto)
}
return false
}
ES6实现单向链表(LinkedList)
在 ES6 中实现链表,我们可以实现链表中的节点和链表本身。链表是一种线性数据结构,其中元素以节点的形式存在,每个节点包含数据部分和指向下一个节点的指针。
下面是一个简单的单向链表实现的例子:
// 实现链表中的节点
class ListNode {
constructor(value) {
this.value = value;
this.next = null;
}
}
// 实现链表本身
class LinkedList {
constructor() {
// 节点
this.head = null;
// 链表长度
this.size = 0;
}
// 向链表尾部添加元素
append(value) {
let newNode = new ListNode(value);
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
this.size++;
}
// 根据索引获取链表中的元素
get(index) {
let current = this.head;
let count = 0;
while (current) {
if (count === index) {
return current.value;
}
count++;
current = current.next;
}
return null; // 如果索引超出范围
}
// 根据值查找元素的索引,如果不存在返回-1
indexOf(value) {
let current = this.head;
let count = 0;
while (current) {
if (current.value === value) {
return count;
}
count++;
current = current.next;
}
return -1;
}
// 打印链表
print() {
let current = this.head;
while (current) {
process.stdout.write(current.value + ' -> ');
current = current.next;
}
process.stdout.write('null\n');
}
}
// 使用示例
let list = new LinkedList();
list.append(1);
list.append(2);
list.append(3);
list.print(); // 1 -> 2 -> 3 -> null
console.log(list.get(1)); // 输出 2
console.log(list.indexOf(3)); // 输出 2
这个例子中,我们定义了ListNode类来表示链表的节点,每个节点包含一个值和一个指向下一个节点的引用。LinkedList类表示链表本身,包含头节点和链表的大小。我们实现了一些基本的操作,如添加元素、获取元素、查找元素的索引和打印链表。
LeetCode真题
141. 环形链表
给你一个链表的头节点 head,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
如果链表中存在环,则返回 true。 否则,返回 false。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
题解:
我们可以使用快慢指针,快指针每次走两步,慢指针每次走一步,如果快慢指针相遇,说明有环,否则没有环。
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
let fast = head, slow = head;
while(fast && fast.next) {
fast = fast.next.next;
slow = slow.next;
if(f === slow) return true;
}
return false;
};
当然我们还有另一种结题思路,就是标记法,走个一遍的设置flag,如果遍历查到有flag,说明有环,否则没环。
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
while(head) {
if (head.flag) {
return true;
}
head.flag = true
head = head.next
}
return false;
};
237. 删除链表中的节点
有一个单链表的 head,我们想删除它其中的一个节点 node。
给你一个需要删除的节点 node。你将 无法访问 第一个节点 head。
链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。
删除给定的节点。注意,删除节点并不是指从内存中删除它。
示例 1:
输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9
题解:
我们可以改变 node 的指向,让他 val 和 next 都指向 node.next :
/**
* @param {ListNode} node
* @return {void}
*/
var deleteNode = function(node) {
node.val = node.next.val
node.next = node.next.next
};
206. 反转链表
给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
题解:
我们可以设置 prev 和 current 去保存链表指向,循环的设置到 prev 上:
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let prev = null;
let current = head;
// 循环current
while(current) {
// 缓存下当前节点
const node = current.next;
// 设置当前下一个节点为 prev
current.next = prev;
// 设置 prev 为 当前节点
prev = current;
// current赋值为当前节点
current = node;
}
// [1,2,3,4,5]
// 第一次循环: current -> [2,3,4,5]; current.next -> null; prev -> [1];
// 第二次循环: current -> [3,4,5]; current.next -> [1]; prev -> [2,1];
// 第三次循环: current -> [4,5]; current.next -> [2,1]; prev -> [3,2,1];
// ...
return prev
};