跳表(SkipList)

普通的有序单链表中,查找的时间复杂度是O(N),尽管真正的插入和删除节点的复杂度只有O(1),但都要先去找寻节点。

普通单链表.png

如果是数组的话,那么就可以"二分法"来快速查找,降低时间复杂度。
那么链表能不能与二分法结合起来呢?

跳表采用**空间换时间"的思想,除了原始链表外还保存一些“跳跃”的链表,达到加速查找的效果。

跳表.png

原理

Skip list 的基础是一个有序的链表。但是因为链表在内存中是离散存储的,我们无法在一个有序链表上执行二分查找。

所以,我们决定在这个有序的链表上增加一层索引,用于将这个有序链表一分为二。通过第一层索引可以将查找量减少一半。

同理,我们可以对左边的链表(1->2->3->4) 建一层索引,将左边一分为二。对右边的链表(6->7->8->9->10)也可以进行一样的操作。如此递归下去…… 这样通过付出一些指针的开销,可以将有序链表的查找时间复杂度从 O(n) 降低到和二分查找一样的 O(logn)

如果每次都要“精准地一分为二”,插入、删除某个节点的时候会可能会涉及到调整其它节点的指针索引高度。这会让逻辑变得复杂许多,就像红黑树插入、删除节点可能会涉及子树的“旋转”一样。 Skip list 放弃了精确控制每个节点的索引高度来实现“二分查找”,转而采用一个随机概率规则来确定一个节点的高度。这样,一个节点的插入和删除,只会和它的相邻节点有关,逻辑简单,并且修改范围非常有限(锁粒度可以做到很细),可以实现更高的并发。

要实现随机近似二分查找,我们需要保证一个高度为 h 的节点,有 1/2 的概率是高度为 h+1 的节点 :
高度 >= 1 的节点概率为 1
高度 >= 2 的节点概率为 1/2
高度 >= 3 的节点概率为 1/4。
...
假设一个高度为 h 的节点,有概率 p 是高度为 h+1 的节点。那么一个长度为 n 的 skip list,需要的指针数量为:

因为 ,当 n 趋近无穷的时候, 趋近于 0,因此 。Skip list 的空间复杂度是 。 如果我们想节省内存空间,可以适当的调低 1/2 这个概率,比如 1/3、1/4,从而减少索引指针个数。

和平衡树的比较

实现

Redis 调表中的实现
LevelDB 跳表中的实现。

参考资料
1、

你可能感兴趣的:(跳表(SkipList))