深入理解redis底层数据结构之跳跃表

跳跃表(skipList)

​ 跳跃表是redis中独有的数据结构,在了解它之前,我们先来回顾一下普通的有序链表。

image-20210923094534529

​ 在一串有序链表中,如果我们要查找某个值,我们只能从头节点依次向后遍历直到找到目标值为止。如果目标值位置靠前还可以接受,但如果在链表后半段甚至刚好的最后一个,那么我们就需要遍历整个列表才能找到它了。时间复杂度是O(n)。

​ 这样的特性主要是取决于链表的结构特性,每次只能向后跨一步来寻找。那么如果我们能否对此步骤进行改进,让不相邻的节点之间也存在某种方式可以到达,每次向后跨两步、三步、四步呢?我们来详细对比一下:

​ 如果我们要在下面这个链表中找到值为30的节点,如果按照普通有序链表的遍历方法,那么一共需要5次才能够到达目标节点。

image-20210923100205217

​ 如果我们在不相邻的两个之间也建立连接,每次向后跳两步,如果跳过了目标值则向前退一个,会需要多少次呢?

image-20210923100914721

​ 只需要4次就可以找到目标节点了,但是感觉还是快的不够明显,那么我们不妨再把跳跃步长放大到3,看看会有什么变化。

image-20210923101523623

​ 果然不出人所料,这次仅仅需要3次就可以找到目标节点了,果然步长越长越香,真香!要是把步长设为6,岂不是只需要两步便可以找到了?机智如斯,吾真乃神人也!

img

​ 但是,步长真的越大越好吗?

​ 考虑一种极端情况:如果在上述情况下,步长为6,但我们想要寻找值为3的节点,需要多少步呢?

image-20210923102658282

​ 纳尼?我本来一步就能找到的操作,你居然让我多走了这么多步!

img

问题1到底该如何确定一个合适的步长呢?

​ 可以采取一种试探的方式。开始取一个尽可能大的步长,向后尝试着走一个节点,如果下个节点的值大于所要寻找的节点的值,那么则立即向后退回之前的节点,并尝试将步长缩小进行下一次尝试。

​ 由此又引出了问题2应该采取什么策略来动态调整步长呢?

​ 如果简单的采取依次递减的方式,显然显得有些笨拙,在上面的例子中,如果想要找到值为3的节点,需要尝试五次之多,显然和原来没什么区别。

​ 那么我们是否可以借鉴在数组中二分查找的思想:第一次的搜索步长为整个链表的长度,第二次搜索步长为整个链表长度的1/2,第三次搜索步长为整个链表长度的1/4…依次类推,直到最后步长为1。让我们看下:

image-20210923112133368

​ 举例来说明吧:

​ 假设现在我们想要找到值为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次成功找到。

image-20210923112101449

再看刚才的极端情况,如果我们要寻找值为3的节点呢?情况会不会比刚才好一些呢?
1)以步长为6跳跃到下一个节点发现:35>3

2)以步长为3跳跃到下一个节点发现:11>3

​3)以步长为1跳跃到下一个节点发现:目标节点找到。

​ 可以看到,刚才极度糟糕的极端情况现在也没有那么糟糕了,而且对于链表后半部分的数据查找速度比原来快很多,且在数据量大的情况下优势更加明显。

image-20210923112000084

仔细分析可以发现:其实和二分查找的原理基本一致,但不要求严格二分,复杂度也是(OlogN)。

​ 综上所属,经过我们的构想,我们想象中的经过改进的表结构应该长这个样子:

image-20210923113412755

​ 但是问题又来了 随着链表不断插入元素,可能会有越来越多的指针需要动态生成:

image-20210923114627975

​ 可以看到随着列表中元素的不断插入,每个节点中的指针数量也是在不断增加的。如果想要维护原来的数据结构,则每个节点就需要一个柔性数组来维护这些指针。于是便有了如下的改进:

image-20210923115501689

​ 于是上图中的每这就是跳跃表数据结构的由来。

image-20210923123912083

​ 其中每一个紫色部分单独划分为一个节点,为了方便再加上一些辅助的信息,就得到了现在的跳跃表的结构。

image-20210923134322252

​ 这张图看起来复杂,但是经过上面的一通分析之后,是不是显得不过如此呢?无非就是增加了一些存储信息的字段和一个头节点而已嘛。

image-20210923134855637

​ 现在让我们仔细分析一下:

​ 首先分析一下跳跃表节点里面的字段:

  • sds:动态字符串,顾名思义用来存储字符串的,也就是键所对应的值。
  • score:权重,就是在需要不断比较大小的值。
  • level:用来存放指针的数组,现在多加了一个字段span用来标识当前层的步长值。
  • backward :指向当前节点最下层的前一个节点的指针,头节点和第一个节点的值为null。通俗来讲就是向前倒退时不能跳跃,只能一个一个走。

​ 紧接着是跳跃表的数据结构:

  • head:指向跳跃表的头节点
  • hail:指向跳跃表的最后一个节点
  • level:当前跳跃表中除头节点外的最大高度(数组的长度),头节点高度固定为64。
  • length:当前跳跃表的节点个数(不包括头节点)。
性质
  1. 跳跃表在节点内将指针分层存储,每一层的指针指向跨度不同的节点。
  2. 跳跃表中包含一个头节点,头节点的层高始终为64 。
  3. 跳跃表的高度为除头节点外的最大节点高度。
  4. 跳跃表中每一层都是一个有序链表。
  5. 跳跃表中一个元素若在上层的有序链表中出现,则它一定会在下层的有序链表中出现。
  6. 最低层的有序链表中包含跳跃表中所有的节点,最低层的链表的节点个数就是跳跃表的长度(不包括头节点)。
  7. 每个节点包括一个后退指针,指向最低层的前一个节点。头节点和第一个节点的后退指针为null。

以上所述皆为参考资料后的个人理解。

参考书籍:Redis5:设计与源码分析

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值