跳跃表(skipList)
跳跃表是redis中独有的数据结构,在了解它之前,我们先来回顾一下普通的有序链表。
在一串有序链表中,如果我们要查找某个值,我们只能从头节点依次向后遍历直到找到目标值为止。如果目标值位置靠前还可以接受,但如果在链表后半段甚至刚好的最后一个,那么我们就需要遍历整个列表才能找到它了。时间复杂度是O(n)。
这样的特性主要是取决于链表的结构特性,每次只能向后跨一步来寻找。那么如果我们能否对此步骤进行改进,让不相邻的节点之间也存在某种方式可以到达,每次向后跨两步、三步、四步呢?我们来详细对比一下:
如果我们要在下面这个链表中找到值为30的节点,如果按照普通有序链表的遍历方法,那么一共需要5次才能够到达目标节点。
如果我们在不相邻的两个之间也建立连接,每次向后跳两步,如果跳过了目标值则向前退一个,会需要多少次呢?
只需要4次就可以找到目标节点了,但是感觉还是快的不够明显,那么我们不妨再把跳跃步长放大到3,看看会有什么变化。
果然不出人所料,这次仅仅需要3次就可以找到目标节点了,果然步长越长越香,真香!要是把步长设为6,岂不是只需要两步便可以找到了?机智如斯,吾真乃神人也!
但是,步长真的越大越好吗?
考虑一种极端情况:如果在上述情况下,步长为6,但我们想要寻找值为3的节点,需要多少步呢?
纳尼?我本来一步就能找到的操作,你居然让我多走了这么多步!
问题1
:到底该如何确定一个合适的步长呢?
可以采取一种试探的方式。开始取一个尽可能大的步长,向后尝试着走一个节点,如果下个节点的值大于所要寻找的节点的值,那么则立即向后退回之前的节点,并尝试将步长缩小进行下一次尝试。
由此又引出了
问题2
:应该采取什么策略来动态调整步长呢?
如果简单的采取依次递减的方式,显然显得有些笨拙,在上面的例子中,如果想要找到值为3的节点,需要尝试五次之多,显然和原来没什么区别。
那么我们是否可以借鉴在数组中二分查找的思想:第一次的搜索步长为整个链表的长度,第二次搜索步长为整个链表长度的1/2,第三次搜索步长为整个链表长度的1/4…依次类推,直到最后步长为1。让我们看下:
举例来说明吧:
假设现在我们想要找到值为30的节点:
1)刚开始步长为6,经过尝试发现一次跳跃后的节点值为35,大于我们目标的节点值30了。于是回到上一个节点1,进行下一次尝试。
2)调整步长为6*1/2=3,然后再次进行跳跃,发现跳跃后的节点值为11,小于目标节点的值30,则再次以步长3进行跳跃,到达节点35,发现节点值大于30,则回到上一个节点11。
3)调整步长为3*1/2 =1 ,然后从11节点出发,跳跃到节点15,15<30,再次跳跃到节点30,只用了3次成功找到。
再看刚才的极端情况,如果我们要寻找值为3的节点呢?情况会不会比刚才好一些呢?
1)以步长为6跳跃到下一个节点发现:35>3
2)以步长为3跳跃到下一个节点发现:11>3
3)以步长为1跳跃到下一个节点发现:目标节点找到。
可以看到,刚才极度糟糕的极端情况现在也没有那么糟糕了,而且对于链表后半部分的数据查找速度比原来快很多,且在数据量大的情况下优势更加明显。
仔细分析可以发现:其实和二分查找的原理基本一致,但不要求严格二分,复杂度也是(OlogN)。
综上所属,经过我们的构想,我们想象中的经过改进的表结构应该长这个样子:
但是问题又来了 随着链表不断插入元素,可能会有越来越多的指针需要动态生成:
可以看到随着列表中元素的不断插入,每个节点中的指针数量也是在不断增加的。如果想要维护原来的数据结构,则每个节点就需要一个柔性数组来维护这些指针。于是便有了如下的改进:
于是上图中的每这就是跳跃表数据结构的由来。
其中每一个紫色部分单独划分为一个节点,为了方便再加上一些辅助的信息,就得到了现在的跳跃表的结构。
这张图看起来复杂,但是经过上面的一通分析之后,是不是显得不过如此呢?无非就是增加了一些存储信息的字段和一个头节点而已嘛。
现在让我们仔细分析一下:
首先分析一下跳跃表节点里面的字段:
-
sds
:动态字符串,顾名思义用来存储字符串的,也就是键所对应的值。 -
score
:权重,就是在需要不断比较大小的值。 -
level
:用来存放指针的数组,现在多加了一个字段span用来标识当前层的步长值。 -
backward
:指向当前节点最下层的前一个节点的指针,头节点和第一个节点的值为null。通俗来讲就是向前倒退时不能跳跃,只能一个一个走。
紧接着是跳跃表的数据结构:
-
head
:指向跳跃表的头节点 -
hail
:指向跳跃表的最后一个节点 -
level
:当前跳跃表中除头节点外的最大高度(数组的长度),头节点高度固定为64。 -
length
:当前跳跃表的节点个数(不包括头节点)。
性质
- 跳跃表在节点内将指针分层存储,每一层的指针指向跨度不同的节点。
- 跳跃表中包含一个头节点,头节点的层高始终为64 。
- 跳跃表的高度为除头节点外的最大节点高度。
- 跳跃表中每一层都是一个有序链表。
- 跳跃表中一个元素若在上层的有序链表中出现,则它一定会在下层的有序链表中出现。
- 最低层的有序链表中包含跳跃表中所有的节点,最低层的链表的节点个数就是跳跃表的长度(不包括头节点)。
- 每个节点包括一个后退指针,指向最低层的前一个节点。头节点和第一个节点的后退指针为null。
以上所述皆为参考资料后的个人理解。