跳跃列表是什么

跳跃列表 skiplist 是一种有序的数据结构。它在设计上,是通过每个节点中维持多个指向其他节点的指针,达到快速访问节点的效果。

跳跃列表可以在时间复杂度为平均 O(logN) 或者最坏 O(N)两种情况下去查找节点,而且可以通过顺序性操作来批量处理了节点。

跳跃列表应用场景

双向链表、SDS、字典等数据结构都被较广泛地应用在了 Redis 的不同地方,而 Redis 中使用到跳跃列表的地方只有两个,一个是 Redis 的 Zset 数据类型,另一个是作为 Redis 集群节点中用作内部数据结构。

跳跃列表节点结构内容

在看跳跃列表结构内容之前,我们先来看跳跃列表节点结构内容:

具体结构代码:

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

图中的红框就是三个跳跃列表节点

跳跃列表节点的 level 属性为一个数组,数组中每个元素都包含了指向其他节点的前进指针,数组的最大长度为32。程序可以通过这些层里面的前进指针加快访问其他节点的速度。

一般情况下,层的数量越多,访问其他节点的速度就越快。

每次创建一个新的跳跃列表节点的时候,程序会根据幂次定律 ( 越大的数出现的概率越小 )随机生成一个在 1 和 32 之间的值作为 level 数组的大小,也是层的高度。

前进指针

每个层 level 里面的前进指针 forward ,用于从表头向表尾访问访问节点。一般前进指针是指向下一个跳跃表节点的某个层,否则就是 NULL 值。

跨度

每个层 level 里面的 span 属性就是跨度,用于记录两个跳跃列表节点之间的距离。当前进指针指向 NULL 值的时候,此时的跨度都是为0。

跨度这个属性的作用在于:当在查找某个节点的过程中,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳跃表中的排位,即是排名第几个。

后退指针

跳跃列表节点中的 backward 属性就是后退指针,用于从表尾向表头访问访问跳跃列表节点。

后进指针只有在第一个跳跃列表节点的时候,指向是 NULL 值,其他节点基本是指向了前一个节点的内存地址。

和层里面的前进指针不同的是,每个节点的后退指针只有一个且每次只能退至前一个节点,而前进指针在每个节点里面可以是多个且可以每次可以前进span 个节点。

分值

跳跃列表节点中的 score 属性就是分值,它是一个 double 类型的浮点数,跳跃列表中的所有节点排序规则就是按照分值从小到大排序的。

成员对象

跳跃列表节点中的 obj 属性就是成员对象,它是一个指向了一个字符串对象的指针,这个字符串对象就是保存着一个 SDS 结构的值。

在同个跳跃列表里面,每个节点保存的成员对象必须保证是唯一的,但是多个节点的分值是可以一样的,当分值相同的节点,它们将按照成员对象在字典序中的大小来排序,小的在跳跃列表的节点前面,大的在后面。

跳跃列表结构内容

在上面多个的跳跃列表节点结构组成下,就可以组成一个跳跃列表了。

跳跃列表结构具体代码:

typedef struct zskiplist {    // 表头节点和表尾节点    struct zskiplistNode *header, *tail;  // 表中节点的数量    unsigned long length;    // 表中层数最大的节点的层数    int level;} zskiplist;

一个具体跳跃列表结构大概情况图:

属性 header、tail 都是指针,分别指向了跳跃列表的表头节点以及表尾节点,通过这两个属性,让查找表头表尾的节点时间复杂度为 O(1)

属性 length 代表了该跳跃列表有多少个节点,查找跳跃列表的长度的时候,时间复杂度也为 O(1)

属性 level 则保存了跳跃列表节点中层高最大的那个节点的层数量,这里的节点中是不包括表头节点的。获取节点中层高最大的那个节点的层数量时间复杂度依旧是 O(1)

跳跃列表的相关实现

创建跳跃列表

创建跳跃列表的时候,程序为调用 zmalloc 为跳跃列表分配内存空间,然后调用 zslCreate 函数,初始化层数为1,长度为0,tail 指针指向为 NULL ,然后调用 zslCreateNode 函数创建一个跳跃列表节点,其为头节点,接着将头节点的后退指针设置为 NULL ,为头节点里面的32个层,每个层的前进指针设置为 NULL ,跨度设置为0

zslCreateNode 函数的创建过程为先为头节点分配内存空间,然后设置初始化相关属性值,分值为0,成员对象为NULL ,并且生成32个层。

zskiplistNode *zslCreateNode(int level, double score, robj *obj) {     // 分配空间    zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));    // 设置属性    zn->score = score;    zn->obj = obj;    return zn;
}zskiplist *zslCreate(void) {    int j;    zskiplist *zsl;    // 分配空间    zsl = zmalloc(sizeof(*zsl));    // 设置高度和起始层数    zsl->level = 1;   zsl->length = 0;   // 初始化表头节点    // T = O(1)    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {        zsl->header->level[j].forward = NULL;        zsl->header->level[j].span = 0;    }    zsl->header->backward = NULL;    // 设置表尾    zsl->tail = NULL;    return zsl;
}

创建好的一个跳跃列表示意图:

插入跳跃列表节点

插入跳跃列表节点可以分为大概4个步骤:

  1. 在各个层寻找新节点的插入的位置
  2. 获取一个随机值作为新节点的层数
  3. 创建新节点并插入跳跃列表
  4. 额外信息更新
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;    unsigned int rank[ZSKIPLIST_MAXLEVEL];    int i, level;    redisAssert(!isnan(score));    // 1. 在各个层查找新节点的插入位置    x = zsl->header;   for (i = zsl->level-1; i >= 0; i--) {      rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];      while (x->level[i].forward &&            (x->level[i].forward->score < score ||                            (x->level[i].forward->score == score &&                           compareStringObjects(x->level[i].forward->obj,obj) < 0){                      rank[i] += x->level[i].span;   x = x->level[i].forward;        }         update[i] = x;   }   // 2.获取一个随机值作为新节点的层数    level = zslRandomLevel();   if (level > zsl->level) {   for (i = zsl->level; i < level; i++) {            rank[i] = 0;            update[i] = zsl->header;            update[i]->level[i].span = zsl->length;        }             zsl->level = level;   }    // 3. 创建新节点并插入跳跃列表   x = zslCreateNode(level,score,obj);    for (i = 0; i < level; i++) {                x->level[i].forward = update[i]->level[i].forward;               update[i]->level[i].forward = x;           x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);       update[i]->level[i].span = (rank[0] - rank[i]) + 1;   }    for (i = level; i < zsl->level; i++) {        update[i]->level[i].span++;    }    // 4. 额外信息的更新// 设置新节点的后退指针   x->backward = (update[0] == zsl->header) ? NULL : update[0];    if (x->level[0].forward)        x->level[0].forward->backward = x;    else        zsl->tail = x;    // 跳跃表的节点计数增一    zsl->length++;    return x;
}

具体讲解:

1. 在各个层寻找新节点的插入的位置

从跳跃列表的最大层数 level 值开始遍历,从表头的对应level 层的前进指针向第一个节点开始遍历,如果节点的分值比新节点的分值大或者分值相同,但节点的成员对象的字典值比新的大,就会把这个节点的前一节点记录到 update 数组里面,以及记录跨度的值到 rank 数组里面。

2. 获取一个随机值作为新节点的层数

为新的节点生成一个随机值,作为新节点的层数。如果这个层数值比其他节点的层数都要大的话,那么将遍历大于原来 level 值的每个层,将初始化这里每个层的跨度为当前 length 的值,并记录到 update 数组里面。也初始化 rank 数组里面对应下标的跨度值。

最后将这个新的层数更新到跳跃列表的 level 属性里面去。

3. 创建新节点并插入跳跃列表

调用 zslCreateNode 函数创建新的节点,并将相关的属性初始值设置好。

遍历新的 level 值,将修改新节点每个层的前进指针为指向之前update数组对应下标记录好的节点,然后将 update 数组对应下标记录好的节点的前进指针指向新节点,以及记录好各节点的新跨度值。

4. 额外信息更新

调整好新节点的后退指针,以及将跳跃列表的 length 长度值加1

跳跃列表的查找

单链表结构的查找,即使链表中的存储数据是有序的,但是它的查找时间复杂度依然是 O(n) ,从表头查到表尾。

这样的结果显然是非常不好的,跳跃列表为了解决查找效率的问题,通过在每个节点上的层上建立前进指针,可以理解为一个索引,直接从第一个节点可能查找到第三第四个节点的值,这样能让原本 O(n) 的效率提高很多。

这样是一种以空间换时间的设计。索引的建立意味着要占更多的内存空间,但是时间查找效率是提高的了。

具体查找过程:

在跳跃列表要查找分值为17的节点的路线如红色箭头所示。

首先跳跃列表都是从最大的层数往小的遍历,先获取第二层的第一个节点,分值为1,比17小,所以继续往第一个节点第二层的前进指针的位置遍历,此时找到分值为9的节点,然后因为还是比17小,继续遍历,获取到分值为21的节点,因为21比17大,此时往分值为9的第一层走,往前进指针方向遍历,最终获取到了分值为17的节点。

如果跳跃列表的层数越高的话,可能查找效率是更快提升的。

如图所示,查找分值为17的节点的时候,可以直接从第一个节点的第三层前进指针获取到分值为17的节点。

通过以上的说明,跳跃列表这样的查找,有点像二分查找法,通过每个层的前进指针直接获取非下一个节点的值,然后对比值大小,加快查找效率。这样的查找效率可以变成时间复杂度为平均 O(logN)

总结

通过对 Redis 跳跃列表的概念,具体结构内容,具体相关操作详细实现的学习,了解到跳跃列表的具体存在,在 Redis 中主要为 Zset 数据结构的实现。

参考:《 Redis设计与实现 》

更多Java后端开发相关技术,可以关注公众号「 红橙呀 」。

Redis底层数据结构——跳跃列表相关推荐

  1. redis 底层数据结构 压缩列表 ziplist

    压缩列表是列表键和哈希键的底层实现之一.当一个列表键只包含少量列表项,并且每个列表项要么就是小整数,要么就是长度比较短的字符串,redis就会使用压缩列表来做列表键的底层实现 当一个哈希键只包含少量键 ...

  2. redis底层数据结构之跳跃表

    redis底层数据结构之跳跃表 redis 的zset有序连表为啥选择用跳跃表? 我们要思考一问题,首先多问问自己为什么,才容易理解它,ps:这是个人观点.首先我们选择的数据结构和算法原因有以下几种: ...

  3. 02 Redis 底层数据结构

    一.不同数据类型存储结构 Redis底层数据结构一共有 6 种,分别是简单动态字符串.双向链表.压缩列表.哈希表.跳表和整数数组.它们和数据类型的对应关系如下图所示: 1 数组与链表的区别 数组和链表 ...

  4. Redis面试题-Redis底层数据结构

    本文参考 嗨客网 Redis面试题 Redis底层数据结构 Redis 的五大数据类型也称五大数据对象,即分别为 string. list. hash. set 和 zset,但 Redis 并没有直 ...

  5. 保存到redis的字符串类型出现斜杆_深入浅出Redis:这次从Redis底层数据结构开始...

    1.概述 相信使用过Redis 的各位同学都很清楚,Redis 是一个基于键值对(key-value)的分布式存储系统,与Memcached类似,却优于Memcached的一个高性能的key-valu ...

  6. Redis底层数据结构详解(一)

    Redis底层数据结构 一.简单动态字符串SDS 1. SDS 2. 为什么Redis没用C语言原生字符串? 2.1 C语言中的字符串 2.2 使用SDS的好处 二.链表linkedlist 三.压缩 ...

  7. Redis底层数据结构详解

    Redis底层数据结构详解 我们知道Redis常用的数据结构有五种,String.List.Hash.Set.ZSet,其他的集中数据结构基本上也是用这五种实现的,那么,这五种是Redis提供给你的数 ...

  8. redis底层数据结构简述

    2019独角兽企业重金招聘Python工程师标准>>> redis的数据库对象有五种,分别是字符串对象(key-value),列表对象(list),哈希对象(hash),集合对象(s ...

  9. Redis——底层数据结构原理

    摘要 Redis 发展到现在已经有 9 种数据类型了,其中最基础.最常用的数据类型有 5 种,它们分别是:字符串类型.列表类型.哈希表类型.集合类型.有序集合类型,而在这 5 种数据类型中最常用的是字 ...

  10. Redis底层数据结构(图文详解)

    目录 前言 Redis为什么要使用2个对象?两个对象的好处 redisObject对象解析 String 类型 1.int 整数值实现 2.embstr 3.raw List 类型 1.压缩链表:zi ...

最新文章

  1. SQL学习之去重复查询
  2. android 更改edittext内容,Android如何实时更改edittext的内容
  3. 【python】Get与Post的区别?(面试官最想听到的答案)
  4. django-url映射给函数给默认值
  5. 【神经网络】给初学者们讲解人工神经网络(ANN)
  6. Codeforces 1105C (DP)
  7. solr 如何实现精确查询
  8. Fixjs——显示基类DisplayObject
  9. Scikit-Learn库概述
  10. windows 7 动态分区转基本分区绿色工具(Conver to basic disk)
  11. 常用的硬件封装库下载网站
  12. Checkra1n 越狱常见问题大全
  13. Hi3519AV100 MPP部署
  14. Mybatis CRUD操作和多表查询
  15. html梯形选项卡,css 梯形tab标签页
  16. 制作自己的印章,和自己的爱人表白纪念!
  17. 埃拉托斯特尼(Eratosthene)筛法
  18. 城市夜空三(续)公布聊天记录第一部分
  19. 洞洞板上的TQFP芯片的焊接模式
  20. linux nginx连接memcache和ngx_http_consistent_hash负载均衡算法

热门文章

  1. 原生JS实现网页导航条特效
  2. 下城投 × 奇点云 |「数智城投驾驶舱」,打造转型示范新样板
  3. 为什么普通人做量化交易会亏钱?
  4. 打开word很慢(无网络时正常)
  5. 武汉市科技企业孵化器认定条件
  6. 单例模式singleton
  7. 企业如何推动流程管理
  8. 「玩物得志 App」:一家典型的云原生企业,如何在创业早期数次“弯道超车”? | 云原生Talk...
  9. dismiss ios pop效果_iOS自定义转场动画-present和dismiss
  10. DDoS攻击:无限战争