链表是在存储空间上具有一定优势的线性结构。
- 它的有序性是通过指针来实现的,即每个元素都有一个指向下一个元素的指针(链表末端元素可能指向 null),所以它不需要连续的内存空间,从而可以节省内存的占用。
- React.js 的 Fiber 算法就是基于链表实现的。
下面的代码实现了一个基础的链表,包括链表的查找、新增和删除功能。
function LinkedList() {
// 声明并定义头结点
var head = {
value: 'head',
next: null
}
// 查找节点
this.find = function(item) {
var currNode = head
while (currNode.value !== item) {
currNode = currNode.next
}
return currNode
}
// 插入节点
this.insert = function (value, pre) {
var newNode = {
value,
next: null
}
var currNode = this.find(pre)
newNode.next = currNode.next
currNode.next = newNode
}
// 移除节点
this.remove = function (item) {
var prevNode = this.findPrev(item)
var currNode = this.find(item)
if (prevNode.next !== null) {
prevNode.next = prevNode.next.next
currNode.next = null
}
}
// 查找前一个节点
this.findPrev = function (item) {
var currNode = head
while (currNode.next !== null && currNode.next.value !== item) {
currNode = currNode.next
}
return currNode
}
}
栈、队列由于操作受限,无法像数组一样通过下标来访问,查找某个元素时只能逐个进行操作,操作效率并不算高。
链表由于指针的存在,使得在操作效率方面有很大的提升空间。
从指针的方向上考虑,既可以单向也可以双向,那么就可以形成具有两个指针的双向链表,还可以让指针的头尾相连,形成双向循环链表。在一个双向循环链表中查找元素,就可以同时往两个方向查找,这使得在查找速度方面会略优于单向循环链表。libuv 中就使用到了双向循环链表来管理任务。
从指针的数量上考虑,还可以通过增加指针的方式来提升操作效率,跳跃表就是这样一种基于链表的数据结构。
下面是一个跳跃表实现原理的例子,在一个链表中建立了 3 层指针。最下一层指针,跨 1 个元素链接;中间一层指针,跨 2 个元素链接;上层指针,跨 4 个元素链接。
1---------->5---------->9->null
1---->3---->5---->7---->9->null
1->2->3->4->5->6->7->8->9->null
假设现在要在链表中找到数字 8,对于简单链表而言,需要查找 8 次。而在上述跳跃表中,只需要 5 步:
- 使用上层指针,找到 5,8 比 5 大,继续;
- 继续使用上层指针,找到 9,8 比 9 小,回退到 5,并且指针层数下移;
- 使用中层指针,找到 7,8 比 7 大,继续;
- 使用中层指针,找到 9,8 比 9 小,回退到 7,并且指针层数下移;
- 使用下层指针,找到 8。
总的来说,跳跃表通过增加链表元素的冗余指针,使用了空间换时间的方式来提升操作效率。在缓存数据库 Redis 中就使用了跳跃表这种数据结构。
参考:《前端高手进阶》