Redis学习(四)跳跃表(skiplist)

9 篇文章 0 订阅
8 篇文章 1 订阅

1. 什么是跳跃表(skiplist)

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
Redis 使用跳跃表作为有序集合键(ZSET)的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis 就会使用跳跃表来作为有序集合键的底层实现。
Redis 只在两个地方用到了跳跃表(skiplist)

  • 实现有序集合键
  • 在集群节点中用作内部数据结构

1.1 为什么需要跳跃表(skiplist)?

对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高,是 O(n)在这里插入图片描述如果我们想要提高其查找效率,可以考虑在链表上建索引的方式。每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引
在这里插入图片描述
这个时候,我们假设要查找节点 8,我们可以先在索引层遍历,当遍历到索引层中值为 7 的结点时,发现下一个节点是 9,那么要查找的节点 8 肯定就在这两个节点之间。我们下降到链表层继续遍历就找到了 8 这个节点。原先我们在单链表中找到 8 这个节点要遍历 8 个节点,而现在有了一级索引后只需要遍历五个节点。
从这个例子里,我们看出,加来一层索引之后,查找一个结点需要遍的结点个数减少了,也就是说查找效率提高了,同理再加一级索引。
在这里插入图片描述
从图中我们可以看出,查找效率又有提升。在例子中我们的数据很少,当有大量的数据时,我们可以增加多级索引,其查找效率可以得到明显提升。
在这里插入图片描述
像这种链表加多级索引的结构,就是跳跃表!

1.2 为什么只有在元素多/长的时候,使用跳跃表?

从上面我们可以知道,跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略。

2. 跳跃表(skiplist)的数据结构

2.1 跳跃表节点(zskiplistNode)

typedef struct zskiplistNode {
    // 后退指针
    struct zskiplistNode *backward;
    
    // 分值
    double score;
    
    // 成员对象
    robj *obj;
    
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;

2.1.1 后退指针(backward)

节点的后退指针(backward 属性)用于从表尾表头方向访问节点,跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。

具体使用流程如下图:
在这里插入图片描述
程序首先通过跳跃表的 tail 指针访问表尾节点,然后通过后退指针访问倒数第二个节点,以此类推,知道后退指针指向 NULL,至此访问结束。

2.1.2 分值(score)

节点的分值(score 属性)是一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大排序。例如后退指针图中的 1.0、2.0、3.0

如果两个成员对象的分值相同,则按照成员对象在字典序中的大小,从小到大进行排序。

2.1.3 成员对象(obj)

节点的成员对象(obj 属性)是一个指针,它指向一个字符串对象,而字符串对象则保存着一个 SDS 值。例如后退指针中的 obj#1、obj#2、obj#3

在同一个跳跃表中,各个节点保存的成员对象必须是唯一的。

2.1.4 层

skiplistNode 中的 level 数组可以包含多个 zskiplistLevel 元素,程序可以通过这些层来加快访问其他节点的速度,一般来说,level 数组包含的元素越多,访问其他节点的速度越快。
每次创建一个新的跳跃表节点的时候,程序都会随机生成一个介于 1-32 之间的值作为 level 数组的大小,这个大小就是层的“高度”。

2.1.4.1 前进指针(forward)

前进指针用于从表头表尾方向访问节点,如图虚线和数字所示,图中只描述了其中一种遍历路线。
在这里插入图片描述

2.1.4.2 跨度

层的跨度(level[i].span 属性)用于记录两个节点之间的距离。

  • 两个节点之间的跨度越大,它们相距的距离越远;
  • 指向 NULL 的所有前进指针的跨度都为 0

2.2 跳跃表(zskiplist)

使用跳跃表(zskiplist)可以在跳跃表节点的基础上,更方便地对整个跳跃表进行处理,比如:

  • 快速访问跳跃表的表头节点表尾节点
  • 快速获取跳跃表节点的数量
typedef struct zskiplist {
    // 表头节点和表尾节点
    struct skiplistNode *header, *tail;
    
    // 表头节点的数量
    unsigned long length;
    
    // 表中层数最大的节点的层数
    int level;
} zskiplist;
  • headertail 指针分别指向跳跃表的表头和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的复杂度为 O(1)
  • length:用于记录节点的数量
  • level:用于获取跳跃表中层高最大的那个节点的层数量。

3. 跳跃表 API

函数作用时间复杂度
zslFree释放给定跳跃表,以及表中包含的所有节点O(N),N 为跳跃表的长度
zslInsert将新节点添加到跳跃表中平均O(logN),最坏O(N),N 为跳跃表的长度
zslDelete将给定节点从跳跃表中删除平均O(logN),最坏O(N),N 为跳跃表的长度
zslGetRank返回包含给定成员和分值的节点在跳跃表中的排位平均O(logN),最坏O(N),N 为跳跃表的长度
zslGetElementByRank返回跳跃表在给定排位上的节点平均O(logN),最坏O(N),N 为跳跃表的长度
zslFirstInRange给定一个分值范围,返回跳跃表中第一个符合这个范围的节点平均O(logN),最坏O(N),N 为跳跃表的长度
zslLastInRange给定一个分值范围,返回跳跃表中最后一个符合这个范围的节点平均O(logN),最坏O(N),N 为跳跃表的长度
zslDeleteRangeByScore给定一个分值范围,删除跳跃表中所有在这个范围之内的节点O(N),N 为被删除节点的数量
zslDeleteRangeByRank给定一个排位范围,删除跳跃表中所有在这个范围之内的节点O(N),N 为被删除节点的数量

时间复杂度 ≠ O(1) 的 API

4. 参考

5. 其他相关文章

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值