跳跃表基本实现原理

背景

在元素有序的情况下,数组可以基于二分查找快速定位元素,而普通链表就比较尴尬了,定位元素的时间复杂度还是 O(N)。

那有没有办法给有序链表加速呢?跳表便是有序链表的升级版,拥有较高的读写性能,且相比于红黑树和平衡树,实现复杂度也更低。

而 Redis 的有序集合底层的数据结构之一就是跳表,本文主要讲述跳表的基本实现原理。

跳表

下图展示了跳表的示例结构:

可以看到跳表通过在原始链表上层建立索引,并通过指针将索引节点串联起来,最终达到跳跃查找的效果。

下面展示了一个 SkipNode 的数据结构:

每个节点都有一个 key 值,作为节点排序以及定位的依据,value 则保存节点数据,另外还会维护 down,right 指针,用于遍历节点。

查询

查找节点的过程相对简单,设置一个临时节点 temp = head,流程如下:

  • 从头结点出发,如果当前节点的key与查询的key相等,那么返回当前节点
  • 如果key不相等,且右侧为null,那么证明只能向下,temp = temp.down
  • 如果key不相等,且右侧节点key小于当前节点key,证明还可往右,temp = temp.right
  • 如果key不相等,且右侧节点key大于当前节点key,证明目标节点在当前节点与右侧节点之间,此时向下,temp = temp.down

下图中红色节点展示了查找【节点 8】 的路线:

删除

删除操作的核心有两点:

  • 找到待删除节点的前驱节点
  • 每一层的待删除节点都需要删除

设置 temp = head,流程如下:

  • 如果temp右侧为null,那么temp=temp.down
  • 如果temp右侧不为null,并且右侧的key等于待删除的key,那么先删除节点,再向下 temp=temp.down,为了删除下层节点
  • 如果temp右侧不为null,并且右侧key小于待删除的key,那么temp向右temp=temp.right
  • 如果temp右侧不为null,并且右侧key大于待删除的key,那么temp向下temp=temp.down

下图展示了【节点 7】被删除的过程,其中红色节点是查找前驱节点的路线,自顶向下逐层删除节点。

插入

插入操作较为复杂,核心点是需要考虑索引的维护,如果严格按照上一层索引个数是当前层索引个数的 1/2 规则,在插入的过程中会频繁涉及大范围索引重建,对性能影响较大。

跳表舍弃了理想状态的索引结构,而是采用随机的方式决定新增节点在上层是否需要建立索引,在保证一定索引格式的同时也极大缩小了索引维护的性能开销。

插入节点的过程一般是由下往上,从底层开始插入,如何找到待插入节点的前驱节点进行插入操作,这边可以使用栈将定位待插入位置的过程中途经的下行节点记录下来。

另外如果插入的元素不断向上建立索引,最终导致索引层数超过了原跳表的最大层数,需要同时维护新的 head 指针。

流程如下:

  • 找到待插入位置,从底层开始插入
  • 完成当前层插入后,判断是否需要向上建立索引,首先判断当前索引层级是否大于最大值,如果大于则停止建立,否则通过随机数逻辑判断是否继续向上建立索引,如果随机数不满足要求,也停止建立索引
  • 不断重复第二步,直到概率退出或者索引层数大于最大索引层

下图展示了插入【节点 6 】的过程,其中因为新增节点的索引高度超出了原来索引的最大高度,所以需要对应新增一个 head 节点;绿色节点则作为下行节点被记录下来。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值