常用抽象数据结构系列——1、跳跃表

概述

这个系列我打算整理一些常用 抽象 数据结构,抽象是指不通过源码,尽可能通过表图文字的形式说清楚它的规则,通过这些规则加深对它的理解。

本篇所整理的跳跃表对我来说不怎么常见,因为我几乎没有听过或用过,是最近在整理 redis 相关知识时遇到的概念,下面我们直接进入正文:


跳跃表

跳跃表本质上也是一种查找结构,它的目的是快速高效的根据 key 查找某个节点。顾名思义,它的效率高是由于跳跃的过程,跳过很多不需要遍历的节点,提升整体查询效率。

其中它一般是这样表示的:SkipList,也就是说:跳跃表的本质也是链表。并且跳跃表维护的结构必须是有序的,节点内必须包含某个属性可以按照规则进行大小排序,否则使用跳跃表不会提升任何效率。具体原因我们往下看:


背景

既然说了跳跃表的本质是有序链表,下面我们先看一张图:
有序链表
上图是一个非常简单的有序链表,其中链表间按照数字从小到大的顺序进行连接。

在上述链表中,当我们需要查询某个数字节点是否存在时,需要从头到尾依次遍历,直到找到第一个等于它 (找到) 或大于它 (没有找到) 的节点。整个查找过程的时间复杂度为 O(n),同样添加、修改、删除等操作同理。

假如我们把上述链表改造为除原来的指针,每相邻一个节点间也存在指针的样式。具体结构如下:
改造后的链表
此时,除原来的链表外,相邻一个节点之间形成了新的链表,这个新链表节点的数量是原链表节点数量的一半,此时假设我们需要查询值为22的节点,具体步骤如下:

  1. 根据上面的链表(相邻一个节点指针形成的链表),判断第一个节点 7 小于 22,向前遍历
  2. 判断节点 19 小于22,向前遍历
  3. 判断节点 26 大于22,由此得出节点处于 19 和 26之间
  4. 根据下面的链表(相邻节点指针形成的链表),判断 19 的下一个节点 22 即是所求,返回节点

从步骤可以看出,这次的查询操作 跳过 遍历 3号、11号节点。和上面单链表的遍历方式相比,少遍历了两个节点,提高整体查询效率。

假如我们继续改造,每相邻两个节点也存在指针连接。改造后的结构如下:
改造后的链表2
此时,假设我们要查询值为22的节点,具体步骤如下:

  1. 根据最上方的链表,判断第一个节点 19 小于 22,向前遍历
  2. 19 节点的最上层链表指向NULL。换第二层的链表判断,此时 26 大于 22,说明所找链表在19 - 26 期间
  3. 判断 19 节点的下一个节点,正是 22 找到所求

此时,只遍历了19、26、22三个节点就找到了所求,跳过了更多的节点,整体查询效率更高了。从这里就可以看出,链表的层数越多,整体查询效率就越高


SkipList

在正式介绍 SkipList 前,我先回答前面提出的问题:为什么跳跃表一定是有序链表?

  • 如果链表本身就无序的话,跳跃过再多的节点也没有意义,因为链表前后节点没有任何关系。在跳跃表中最少得包含一个字段用来排序

回到跳跃表,跳跃表就是根据背景中多层链表的设计模式启发得来的。简单来说,多层链表通过上层链表缩小查询范围,提高整体查询效率。我先总结以下多层链表的特点:

  • 上层链表节点数总是比下层链表节点数小
  • 上层链表节点数总是最底层链表节点数的 1 / n
  • 每次查询总是从最高层开始,依次向下,一级一级查询

在上述特点中,SkipList 继承了它的第一条和第三条,第二条没有继承。因为第二条虽然简化了新链表创建的逻辑,但同时为其它除查询外的操作带来了很大困扰:

  • 添加新节点后,为了维持原来每隔 n 个节点通过链表连接的规则,新节点后的每一层链表都必须重新分配,
  • 删除节点和添加节点同理,同时删除节点还会影响前几个节点的指针连接
  • 修改节点如果修改了排序用的字段值,影响更加严重。

举个简单的例子:
多层链表插入新节点
从上图可以看出,添加新节点后,新加节点后的所有二层链表都需要更新,删除和修改操作同理,这个过程是非常耗费资源的。这也是跳跃表和多层链表最大的不同。

多层链表为了解决该问题,它不要求上下层链表间有固定的对应关系,而是对每个节点随机一个层数。假如随机出的层数为3,就把该节点链接到一层到三层这三个链表,以此来保证上层链表节点数不会大于下层节点数。下面我通过图展示 SkipList 创建的步骤:
上传步骤
从上述 SkipList 的创建过程可以看出:SkipList 中每个节点的层数是随机的,并且新插入的节点只需要连接前后节点,不影响其它节点的指针,这也是 SkipList 相比多层链表最大的优势。

假设我们现在需要查找节点23,下面我通过文字的形式模拟整个查询过程:

  1. 根据最上层链表,判断节点 7 小于 23,继续向前遍历
  2. 节点 7 的最上层指针指向 NULL,根据第二高层链表判断,此时 37 大于 23,说明节点可能在此期间
  3. 根据第三高层判断,节点 7 的下一个节点 19 小于 23,继续向前遍历
  4. 根据第三高层,节点 19 的下一层节点 37 小于 23,通过第四高层,也就是最底层判断
  5. 最底层,依次遍历,发现没有找到,返回NULL

总结来说,除了链表之间长度无关外,跳跃表基本和多层链表相同,都是通过高层链表缩小范围,提高整体查询效率。


性能

跳跃表性能的好坏主要由节点的层数决定,层数过高或者过低都会导致不良的结果。

  • 层数过高时,节点所占内存较多,物理消耗严重
  • 层数过低时,高层链表遍历时,跳跃过的节点少,整体效率不高

这里我简单给出跳跃表节点层数计算的随机逻辑:

  1. 首先每个节点肯定包含第一层指针
  2. 如果一个节点有n层,那么它有第 n+1 层的概率为 p
  3. 每个节点的层数都有最大限制,我们记最高层为 MAXLEVEL

根据伪代码可以表示如下:

int getLevel(){
	int level = 1;
	while (level < MAXLEVEL && Math.random() < p) {
		level = level + 1;
	}
	return level;
}

根据不同的业务场景,p 和 MAXLEVEL 可以取不同的值,不同的值也会导致不同的效率结果。

总的来说,跳跃表的平均时间复杂度为 O(log n),整体效率还是相当不错的。关于它时间复杂度的具体计算逻辑,可以参考本篇末尾给出的链接内容。


跳跃表、平衡树、哈希表的比较

在正式比较开始前,我先提一下跳跃表也是 key-value 型的。

  • key:跳跃表中用来排序的属性,每个跳跃表节点都必须包含该属性
  • value:除 key 外其他属性都可以称为值,主要保存核心数据

有了上面的概念,我们正式开始比较:

  1. 跳跃表和平衡树是 有序的,更适合进行范围查询
  2. 在范围查询时,跳跃表相比平衡树更加简单,因为平衡树通过 中序遍历 获得依次从小到大的节点,跳跃表根据最底层链表依次遍历即可。中序遍历相比单链表复杂了很多
  3. 跳跃表的插入、删除操作更高效,因为只需修改被操作节点前后两个节点的属性。而平衡树操作完成后,为了让树平衡,可能需要进行 旋转,修改整个树的结构
  4. 在查询效率上,跳跃表和平衡树都是 O(log n),哈希表为 O(1),整体来说,哈希表在查询单个 key 时更高效
  5. 从算法难度上来说,跳跃表的实现比平衡树简单许多

参考:
https://juejin.im/post/57fa935b0e3dd90057c50fbc#heading-2
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值