跳表的思想起点来自二分搜索,大家都知道一个排好序的数组进行二分搜索是很容易的,就是每次选择中间的数据,看看比要搜索的数据大还是小,大的话就向右搜索,小的话就向左搜索,复杂度是惊人的O(log(n)),比如42 亿个数据中用二分查找一个数据,最多也只需要比较 32 次。
但是对于redis这样的工具来说,会有频繁的插入删除操作,我们知道数组的插入删除操作性能是比较低的,因为需要O(n)次的移动。而对插入删除友好的是链表,只要断开指针前后调一下就好了,但问题是找到需要插入的位置是很麻烦的,需要遍历一遍链表,所以复杂度也是O(n)。
那有没有加快链表找到当前数据的办法呢?其实也不新鲜,就是加索引,于是redis祭出了跳表神器。
作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。如果你了解红黑树、AVL 树这样平衡二叉树,你就知道它们是通过左右旋的方式保持左右子树的大小平衡,而跳表是通过随机函数来维护前面提到的“平衡性”。
当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。如何选择加入哪些索引层呢?
我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。
下面是用js简单实现的跳表生成代码:
const MAX_LEVEL = 8
const PROBABILITY = 0.5
class SNode {
constructor (e) {
this.e = e
this.nexts = []
}
next (level) {
return this.nexts[level]
}
}
class SkipList {
static fromArray (arr) {
if (!arr) {
return null
}
const list = new SkipList()
arr.forEach(e => {
list.add(e)
})
return list
}
constructor () {
this.size = 0
this.curLevel = 0
this.header = new SNode(null)
}
add (e) {
let level = this.randomLevel()
if (level > this.curLevel) {
this.curLevel = level
}
const newNode = new SNode(e)
let currentNode = this.header
while (level >= 0) {
currentNode = this.findPosForAdd(e, currentNode, level)
newNode.nexts[0] = currentNode.next(level)
currentNode.nexts[level] = newNode
level--
}
this.size++
return true
}
findPosForAdd (e, currentNode, level) {
let nextNode = currentNode.next(level)
while (nextNode) {
if (e < nextNode.e) {
break
}
currentNode = nextNode
nextNode = currentNode.next(level)
}
return currentNode
}
/**
* 该算法的意思是返回1的概率是1/2
* 返回2的概率是1/4
* 返回3的概率是1/8
* 依此类推。
*
* 看成一个分布的话,第0层包含所有节点,第1层含有1/2个节点,第2层含有1/4个节点…
*/
randomLevel () {
let level = 0
while (Math.random() < PROBABILITY && level < MAX_LEVEL - 1) {
++level
}
return level
}
print () {
const headerLevels = this.header.nexts
// 从level高的开始打印
for (let level = headerLevels.length - 1; level >= 0; level--) {
const header = headerLevels[level]
let result = 'h -> '
let cur = header.next(level)
while (cur) {
result += `${cur.e} -> `
cur = cur.next(level)
}
if (result !== 'h -> ') {
result += 'null'
console.log(result)
}
}
}
}
module.exports = SkipList