跳跃表
1. 概述
跳跃表是一种数据结构。它允许快速查询一个有序连续元素的数据链表。跳跃表的平均查找和插入时间复杂度都是O(logn),优于普通队列的O(n)。快速查询是通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集。一开始时,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间。这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止,跳跃表示意图如下所示。
从图中可以看到, 跳跃表主要由以下部分构成:
- 表头(head):负责维护跳跃表的节点指针;
- 跳跃表节点:保存着元素值,以及多个层。
- 层:保存着指向其他元素的指针,高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次。
- 表尾:全部由 NULL 组成,表示跳跃表的末尾。
本篇文章将以redis中的跳跃表为例进行介绍,代码用java进行了重写,为方便看懂,简化了部分流程。
2. 层数
跳跃表是按照层构建的,每一层都是一个有序链表,最底层包含了所有的元素,每一个更高层都包含了指向下层的指针,也可以说高层是下层数据的“索引”。通过对高层数据的检索可以实现类似二分查找的效果,但是要准确判断出数据是否出现在高层通且对之前数据的层次进行调整是一个复杂的过程,要实现的算法可以参考AVL树和红黑树。在跳跃表中,通过概率的方式,近似得出数据的层数,简化操作。
每个更高层都充当下面列表的“快速通道”,在第i层中的元素按某个固定的概率p(通常为1/2或1/4出现在第i+1层),以Redis中的跳跃表为例:
level 1的概率为 0.75
level 2的概率为 0.75 * 0.25
level 3的概率为 0.75 * 0.25 * 0.25
…
level 31的概率为 0.75 * 0.25^30
level 32的概率为 0.75 * 0.25^31
代码如下所示:
1 | private int zslRandomLevel() { |
其中ZSKIPLIST_MAXLEVEL为32,ZSKIPLIST_P为0.25(即1/4),random.nextInt() & 0xFFFF为16位整数,ZSKIPLIST_P * 0xFFFF为16位整数的1/4。从概率学的角度来说,大于ZSKIPLIST_P * 0xFFFF的概率为3/4,即level 1的概率是0.75,level 2,概率是0.75 * 0.25,后面的层次依次类推。
在介绍跳跃表的插入和删除之前,先看下跳跃表的数据结构(参考redis的实现进行了简化):
1 | public class ZSkipList<T extends Comparable<? super T>> { |
3. 插入
插入操作的步骤:
- 1)找出插入结点每一层的前驱结点及排名;
- 2)随机生成插入结点的层数;
- 3)在每一层链表中插入结点,修改结点跨度;
- 4)更新结点数。
代码如下所示:
1 | /** |
以插入12, 45, 63, 21, 99, 87, 23, 47, 30, 50数组为例,分析插入结点的流程,图形的格式说明:head[00](01)–>12(00),定义为head[层数](跨度,当前结点与下一个结点之间相隔结点数)–>下一个结点(跨度)。
1)插入12,层数为1,当前只有一层,12为最底层的第一个结点,头结点到12中间的跨度为1(包括12在内)。
1
2
3new node = 12, level = 1
head[00](01)-->12(00)2)插入45,层数为2,跳跃表新增一层,在两层中插入45,其中第二层头结点到45结点的跨度为2(包括45在内)。
1
2
3
4new node = 45, level = 2
head[01](02)----------->45(00)
head[00](01)-->12(01)-->45(00)3)插入63,层数为1,只插入到第一层。
1
2
3
4new node = 63, level = 1
head[01](02)----------->45(01)
head[00](01)-->12(01)-->45(01)-->63(00)4)插入21,层数为2。
1
2
3
4new node = 21, level = 2
head[01](02)----------->21(01)-->45(01)
head[00](01)-->12(01)-->21(01)-->45(01)-->63(00)5)插入99,层数为1。
1
2
3
4new node = 99, level = 1
head[01](02)----------->21(01)-->45(02)
head[00](01)-->12(01)-->21(01)-->45(01)-->63(01)-->99(00)6)插入87,层数为1。
1
2
3
4new node = 87, level = 1
head[01](02)----------->21(01)-->45(03)
head[00](01)-->12(01)-->21(01)-->45(01)-->63(01)-->87(01)-->99(00)7)插入23,层数为1。
1
2
3
4new node = 23, level = 1
head[01](02)----------->21(02)----------->45(03)
head[00](01)-->12(01)-->21(01)-->23(01)-->45(01)-->63(01)-->87(01)-->99(00)8)插入47,层数为2。
1
2
3
4new node = 47, level = 2
head[01](02)----------->21(02)----------->45(01)-->47(03)
head[00](01)-->12(01)-->21(01)-->23(01)-->45(01)-->47(01)-->63(01)-->87(01)-->99(00)9)插入30,层数为1。
1
2
3
4new node = 30, level = 1
head[01](02)----------->21(03)-------------------->45(01)-->47(03)
head[00](01)-->12(01)-->21(01)-->23(01)-->30(01)-->45(01)-->47(01)-->63(01)-->87(01)-->99(00)10)插入87,层数为4。
1
2
3
4
5
6new node = 50, level = 4
head[03](07)-------------------------------------------------------->50(03)
head[02](07)-------------------------------------------------------->50(03)
head[01](02)----------->21(03)-------------------->45(01)-->47(01)-->50(03)
head[00](01)-->12(01)-->21(01)-->23(01)-->30(01)-->45(01)-->47(01)-->50(01)-->63(01)-->87(01)-->99(00)
4. 删除
删除操作的步骤为:
- 1)找出删除结点每一层的前驱结点;
- 2)将前驱结点的后继结点指向删除指点的后继结点,并修改前驱结点的跨度;
- 3)删除空的层并对结点数减一。
跳跃表的删除实际就是单向链表的删除,其代码如下:
1 | /** |
删除实例如下:
1 | 删除前的跳跃表: |
在每一层的链表中都会删除30这个结点,同时更新前驱结点的跨度。因为第4层只有30这个结点,删除之后,该层没有结点,就要减少跳跃表的层数。
6. 总结
跳跃表的发明者对其的评价:
跳跃列表是在很多应用中有可能替代平衡树而作为实现方法的一种数据结构。跳跃列表的算法有同平衡树一样的渐进的预期时间边界,并且更简单、更快速和使用更少的空间
跳跃表相对平衡二叉树而言,实现简单,又能在近似O(logn)时间复杂度的情况下实现数据的检索和插入,这或许也是在redis中使用跳跃表代替红黑树的原因之一。
参考: