1.前言

  自己学跳跃表是因为当初听人说想要找一份高薪工作, Redis跳跃表是要知道的. 当时学的时候也是网上的文章反复看, 花了几个晚上才彻底弄明白, 所以在此记录一下吧, 为了下次面试好回顾

2.跳跃表基本概念准备

跳跃表是有序集合(zset)的底层实现之一。

2.1跳跃表的数据结构

跳跃表zskiplist定义在server.h中

header; 跳跃表的表头节点
tail: 指向跳跃表的表尾节点
level: 记录目前跳跃表内, 层数最大的那个节点的层数(表头节点的层数不计算在内. 因为它的层数为level31: 从level0开始, 所以是32层)
length: 记录跳跃表的长度, 也就是, 跳跃表目前包含节点的数量(表头节点不计算在内)

2.2跳跃表节点的数据结构

跳跃表的节点zskiplistNode定义在server.h中, 定义如下

robj: RedisObject的别名, 在跳跃表中它的类型是sds字符串
score: 浮点类型的数值
backward: 后退指针, 指向跳跃表当前节点的前一个节点的指针
level[]: 数组中的一个元素包含下列两项
  forward: 前进指针
  span: 当前层跨越的节点数量

节点按分值从低到高排列, 分值相同时按 robj字典顺序排列, 而不是按对象本身的大小
一个节点在每一层都有一个forward指针(各层的forward指针可能相同, 可能不同)
如图:

那么对应节点8而言:
  节点8的level0的forward为节点9, span值为1
  节点8的level1的forward为节点12, span值为4
  节点8的level2的forward为节点16, span值为8
所以, forward指针指向的是当前节点的当前level层所能指向的最右的节点(其最大的level层 >= 当前节点的当前level层), 而span就是这个过程中跨越的节点数. 将上述节点8的三个level层依次带入, 应该就能理解上面这句话了.

2.3Rank

它代表每个节点在跳跃表中的相对位置(类似数组下标)
如上图中节点8的rank值为 8 , 从第一个节点开始(不包含头节点) rank = 1, 然后依次类推.

3.跳跃表的创建

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;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;
}

其中:
  ZSKIPLIST_MAXLEVEL,这个是跳跃表的最大层数,源码里通过宏定义设置为了32,也就是说,节点再多,也不会超过32层(level31)
  header节点的初始化: 创建跳跃表时, 初始化的header节点的level数组是有32层(level31)的, 且每一层的forward指向的都是null, 这里的知识在后面节点插入时会用到.
  而一般节点的level层数是在节点插入到跳跃表时 随机给定的, 根据一个随机算法:

上图是随机给定每一层的概率

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));x = zsl->header;// 记录沿途访问的节点,并计数 span 等属性// 平均 O(log N) ,最坏 O(N)for (i = zsl->level-1; i >= 0; i--) {/* store rank that is crossed to reach the insert position */rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];// 右节点不为空while (x->level[i].forward &&                   // 右节点的 score 比给定 score 小(x->level[i].forward->score < score ||      // 右节点的 score 相同,但节点的 member 比输入 member 要小(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;}/* we assume the key is not already inside, since we allow duplicated* scores, and the re-insertion of score and redis object should never* happpen since the caller of zslInsert() should test in the hash table* if the element is already inside or not. */// 因为这个函数不可能处理两个元素的 member 和 score 都相同的情况,// 所以直接创建新节点,不用检查存在性// 计算新的随机层数level = zslRandomLevel();// 如果 level 比当前 skiplist 的最大层数还要大// 那么更新 zsl->level 参数// 并且初始化 update 和 rank 参数在相应的层的数据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;}// 创建新节点x = zslCreateNode(level,score,obj);// 根据 update 和 rank 两个数组的资料,初始化新节点// 并设置相应的指针// O(N)for (i = 0; i < level; i++) {// 设置指针x->level[i].forward = update[i]->level[i].forward;update[i]->level[i].forward = x;/* update span covered by update[i] as x is inserted here */// 设置 spanx->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);update[i]->level[i].span = (rank[0] - rank[i]) + 1;}/* increment span for untouched levels */// 更新沿途访问节点的 span 值for (i = level; i < zsl->level; i++) {update[i]->level[i].span++;}// 设置后退指针x->backward = (update[0] == zsl->header) ? NULL : update[0];// 设置 x 的前进指针if (x->level[0].forward)x->level[0].forward->backward = x;else// 这个是新的表尾节点zsl->tail = x;// 更新跳跃表节点数量zsl->length++;return x;
}

那么如何理解上述函数就是关键了.

4.1两个关键的数组

这两个数组一定要理解!!!
update[]: 数组的每个元素update[i] --> XXX节点: XXX节点的第i层的forward指向插入节点
rank[]: 每一层中XXX节点的rank值(后面用来计算span的)

update[]: 记录寻找元素过程中, 每层能到达的最左节点(XXX节点)
rank[]: 最左节点的rank

补充: 最左节点是对插入节点而言, 如果根据后面介绍的, 从header开始遍历, 寻找update[i], 则可以看成最右节点.

4.2插入一个新节点时涉及的操作

  前面说过, 跳跃表插入节点时, level是随机给的, 我们这里假设插入节点的分值为9.5, 随机生成的层数是2, 那么此时跳跃表如图:

对跳跃表而言, 插入一个新节点所涉及的操作有:
  插入节点每一层的forward和span获取
  每一层最左节点的forward和span更新

插入节点9.5之后:
level0:
  插入节点的forward可以根据score排序得到, span为1
  update[0]是插入节点的backward, span为1
level1:
  8 ~ 12之间的span = span8span_{8}span8​ + 1 =>
  8 ~ 9.5 + 9.5 ~ 12 = span8span_{8}span8​ + 1 =>
  求出8 ~ 9.5,也就同样能求出 9.5 ~ 12
  8 ~ 9.5 = 8 ~ 9 + 1
      =rank(节点9) - rank(节点8) + 1

  span8span_{8}span8​表示的是插入之前, 节点8在level1的span值

对上述公式的解释:
  我要更新插入节点的level1层的最左节点的forward和span, forward为update[1], span就需要根据上述公式计算得到, 而刚好节点9为update[0], 节点8为update[1], 所以:
   =rank(update[0]) - rank(update[1]) + 1
同理: 对于更新插入节点的第i层的最左节点的span值
  抽象为: rank(update[0]) - rank(update[level.i]) + 1
      =rank(0) - rank(i) + 1
这个公式求得的是: 插入节点的第i层对应的最左节点的第i层span值.
此时我们再来看看这段:

每一层对应的最左节点的forward更新为update[i], span可以根据rank(0) - rank(i) + 1计算来更新
插入节点第i层的forward为update[i]在插入之前的forward(插入之后update[i]的第i层forward指向的是插入节点), span值获取: “8 ~ 9.5根据公式计算得到了, 9.5 ~ 12自然也就知道了”. 这么说不知道能不能理解.

而这个计算公式在源码中也有出处:

所以关键就是找到插入节点的update[] 和 rank[].

4.2遍历,记录update和rank

对应源码:

    // 记录寻找元素过程中,每层能到达的最右节点zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;// 记录寻找元素过程中,每层所跨越的节点数unsigned int rank[ZSKIPLIST_MAXLEVEL];int i, level;redisAssert(!isnan(score));x = zsl->header;// 记录沿途访问的节点,并计数 span 等属性// 平均 O(log N) ,最坏 O(N)for (i = zsl->level-1; i >= 0; i--) {/* store rank that is crossed to reach the insert position */rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];// 右节点不为空while (x->level[i].forward &&                   // 右节点的 score 比给定 score 小(x->level[i].forward->score < score ||      // 右节点的 score 相同,但节点的 member 比输入 member 要小(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;}

update和rank数组的值可以通过一次逐层的遍历确定
从跳跃表当前的最大层数开始遍历, 遍历到最底层为止.
     update[]      rank[]

length - 1层:
  X的head的level-1层(从0层开始, 这里的level是指跳跃表中当前的最大层数)指向节点X1(当前跳跃表内最高层), 判断:
  1.X1的level-1层的forward是否为null
    如:从 X1到表尾的所有节点的level层数都小于 X1的level层数, 此时length - 1 层的forward为null
  2.X1的score是否比插入节点的大
  满足其中之一 --> 跳出循环
  update[length -1] = header, rank[length - 1] = 0
  不满足其中之一 -->
  找到X1的length-1层的forward指向的节点X2(能到这 说明: X1与X2的level层数相等的), 判断:
  是否满足上述条件之一
  满足 --> 跳出循环
  update[length - 1] = X1, rank[length - 1] = X1在表中的rank值
length - 2层:
  X1的次高层的forward指向节点X3, 判断:
  …
  不满足 -->
  X3的最高层forward指向的节点 X4满不满足, X4的最高层forward指向的节点满不满足 假设满足:
  update[length - 2] = X4, rank[length - 2] = X4在跳跃表的rank值
length -3层:
  X4的次高层…
此时我们得到完整的update[] 和 rank[]

结合图示理解:

保姆级说明:

结合上述两幅图, 在插入节点后是如何通过一次遍历获取update[], rank[]数组的:
  1.首先在插入9.5节点之前, 跳跃表中level的值为3, 所以我们来到header节点的level-1层, 也就是level2, 其中的forward指向的是节点8(为什么header的level2中会有值, 下面会说明, 别急)
  2.判断循环条件, 不满足, 继续. 节点8的level2层的forward指向节点16, 满足. 此时update[2] = 节点8, rank[2] = 8
  3.从节点8的次高层level1出发, forward指向的是节点12, 判断循环条件, 满足. 此时update[1] = 节点8, rank[1] = 8
  4.从节点8的次次高层level0出发, forward指向的是节点9, 判断循环条件, 不满足, 继续, 节点9的最高层forward指向节点10, 判断循环条件, 满足, update[0] = 节点9, rank[0] = 9

  如果上述内容都理解了, 相信跳跃表的插入流程在脑子里大致是能跑同了, 但感觉还有些模糊地带, 接下来我们就来探索这些模糊地带, 全面理解Redis跳跃表.

5.如果插入节点随机生成的层数比当前跳跃表的最大层数大

  上文中提到过: 为什么header的level2中会有值. 接下来我们就来解释这个问题
  当插入节点随机生成的层数(j)比当前跳跃表的最大层数(level)大, 对于update[], rank[]中 数组下标≤ level 的依然可以通过一次遍历得到, 而对于数组下标: level <数组下标 ≤ j的, 此时显然:
  update[level + …] = header, rank[level+ …] = 0
  多出来的这些层的header的span:
    = rank[0] - rank[i] + 1
    =rank[0] + 1

而对于插入节点的那些 大于等于level层的forward = null, 所以也就没有span值

6.更新未涉及到的层

  如果随机生成的层数小于之前跳跃表中的层数, 那么大于随机生成的层数在创建新节点的过程中就没有被操作到(比如上述中, update[2] = 节点8 就没有被使用到,), 对于这些没有操作到的层, 里面的update节点对应的span应当+1(因为插入了一个新节点), forward不变.
  至此: 更新每一个update[i]的forward和span完成

  “物理的科学大厦已经建成, 剩下的只有大厦旁的两朵乌云, 后人只需在此基础上修补, 完善”. 原话找不到了, 凑合用吧!

7.设置后继指针

  针对每一层的调整已经全部完成了, 也就是level数组已经搞定, 接下来, 处理一个backward指针, 首先新节点的backward要指向前一个节点, 然后, 新节点的下一个节点要将backward指向新节点.

8.更新跳跃表节点个数

  最后, 全部搞定, 把跳跃表个数加1即可, 理解了插入, 对跳跃表可以说是基本掌握了. 因为掌握了插入, 也就掌握了跳跃表的其他操作(如 删除).
  其实就是觉得为了面试学到这, 用来装逼应该足够了. 当然时间够的还可以了解一下Redis的一致性hash, Redis的内存淘汰机制, 这两个理解起来会容易很多.
Redis的一致性hash:
https://blog.csdn.net/qq_21125183/article/details/90019034?
Redis的内存淘汰机制: 第9题有稍微说明
https://blog.csdn.net/weixin_43179522/article/details/109318370

9.补充

  跳跃表是一种随机化的数据结构, 在查找, 插入和删除这些字典操作上, 其效率可比拟于平衡二叉树(如红黑树), 大多数操作只需要O(log n)平均时间. 为什么时间能从O(n)提升到O(log n)
  用下图举个例子吧:

  当我们要查找新插入的节点9.5时, 如果不是用的跳跃表, 那么需要从节点1开始, 找到节点2, 然后一直找到节点9.5, 花费的时间为10.
  而如果是跳跃表结构, 我们从header的level-1层出发, 根据span和forward来到节点8, 发现节点8的score < 节点9.5的score, 然后我们接着从节点8的level2出发, 来到节点16, 发现节点16score > 节点9.5的score, 所以我们重新从节点8的level1出发, 来到节点9.5. 找到节点, 返回结果. 时间花费: header -> 节点8 -> 节点16, 节点8 -> 节点9.5 = 3
  总结: 跳跃表根据level数组, 相当于为跳跃表构建了多级索引, 每一层level都是一级索引, 从而减少了查询时间.

10.参考链接

https://blog.csdn.net/u013536232/article/details/105476382?
https://blog.csdn.net/weixin_30398227/article/details/94981429?
https://blog.csdn.net/universe_ant/article/details/51134020?
  在此对这三篇博客的作者提供的思路表示感谢.
  对了, 本来学这个是想面试中装逼用的, 结果愣是没人问. 我真的! 哎, 就像沈佳宜说的: “人生本来很多努力都是徒劳无功的”. 至少它很难, 但你学会了!!!
  最后, 如果觉得通过这篇博客理解了Redis跳跃表, 求个点赞, 收藏. 写博客后才发现, 码字也不是个轻松活.

Redis跳跃表详解相关推荐

  1. 二叉树,平衡二叉树,B-Tree,B+Tree,跳表详解

    二叉树,平衡二叉树,B-Tree,B+Tree,跳表详解 1.二叉查找树(BST) 1.1 二叉查找树概念 1.2 二叉查找树特点 2. 平衡二叉树(AVL) 2.1 平衡二叉树概念 2.2 平衡二叉 ...

  2. 探索Redis设计与实现6:Redis内部数据结构详解——skiplist

    Redis内部数据结构详解(6)--skiplist  2016-10-05 本文是<Redis内部数据结构详解>系列的第六篇.在本文中,我们围绕一个Redis的内部数据结构--skipl ...

  3. Redis内部数据结构详解(2)——skiplist

    Redis里面使用skiplist是为了实现sorted set这种对外的数据结构.sorted set提供的操作非常丰富,可以满足非常多的应用场景.这也意味着,sorted set相对来说实现比较复 ...

  4. Redis面试题-Redis跳跃表

    本文参考 嗨客网 Redis面试题 Redis跳跃表 什么是跳跃表 Redis 中的跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的. 为什么使用跳 ...

  5. Redis AOF 持久化详解

    来自公众号:程序员历小冰 Redis 是一种内存数据库,将数据保存在内存中,读写效率要比传统的将数据保存在磁盘上的数据库要快很多.但是一旦进程退出,Redis 的数据就会丢失. 为了解决这个问题,Re ...

  6. 转-Redis AOF 持久化详解

    转自: https://juejin.cn/post/6844903902991630349 Redis AOF 持久化详解 Redis 是一种内存数据库,将数据保存在内存中,读写效率要比传统的将数据 ...

  7. Redis 事件机制详解

    Redis 采用事件驱动机制来处理大量的网络IO.它并没有使用 libevent 或者 libev 这样的成熟开源方案,而是自己实现一个非常简洁的事件驱动库 ae_event. Redis中的事件驱动 ...

  8. Redis最全详解(一)——基础介绍

    Redis介绍 redis是基于内存可持久化的日志型.Key-Value数据库.redis安装在磁盘,但是数据存储在内存.非关系型数据库NoSql.开源免费,遵守BSD协议,不用关注版权问题. red ...

  9. Redis底层数据结构详解

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

最新文章

  1. 兼容Silverlight4的实用的Silverlight可拖放工具类源代码
  2. 47. Permutations II 1
  3. 【Android 高性能音频】hello-oboe 示例解析 ( Oboe 源代码依赖 | CMakeList.txt 构建脚本分析 | Oboe 源代码构建脚本分析 )
  4. 江南山区腊味香 年味浓
  5. 科大星云诗社动态20210530
  6. Python中装饰器的理解和实现
  7. zookeeper使用和原理探究
  8. Java递归例子——求x的y幂次方
  9. RS232, RS422, RS485 引脚布局区别
  10. springboot怎么返回404_自定义SpringBoot REST API 404返回信息
  11. android之volley学习
  12. 电力系统学习-电力系统及电力模型
  13. 先写接口文档还是先开发
  14. 【Go实战基础】程序里面数据是如何显示到浏览器当中的
  15. 虹科-将人工智能引入电子组装检测
  16. HTML中的图片标签<img>
  17. 电源管理芯片:LED驱动电源芯片的计划及面积
  18. MATLAB将.m文件打包为DLL
  19. 软件灰色按钮 隐藏按钮破解
  20. 平安好医生2019年上半年营收同比增长102%

热门文章

  1. json java对象 简书_Java 对象的 Json 化与反 Json 化
  2. mysql5.7主从同步与读写分离
  3. 程序和计划任务管理( 查看进程ps,控制进程,终止命令进程,top命令,at一次性任务,crontab周期任务)
  4. all any 或 此运算符后面必须跟_用 ANY、SOME 或 ALL 修改的比较运算符
  5. 华为平板wps语音朗读_华为平板M6 10.8英寸综合评测 目前体验最好的安卓平板
  6. int** 赋值_Python的赋值、浅拷贝、深拷贝之间的区别
  7. python web框架对比_Python六大开源框架对比
  8. 纸的大小图解_图解常见纸张开数尺寸印前小常识
  9. python异常处理_Python基础语法案例(Fibonacci):选择结构、循环结构、异常处理结构、代码优化...
  10. 字符串操作 c语言,C语言字符串操作(示例代码)