看完Redis源码还不理解跳跃表吗?( 三 )


在跳表中查询任意数据的时平均时间复杂度就是 O(logn) 。
跳表查询的空间复杂度分析在这里的话,我们假设它的长度为 n,然后按照之前的例子,每两个节点抽一个做成一个索引的话,那么它的一级索引为二分之 n 对吧 。最后如下:

  • 原始链表大小为 n,每 2 个结点抽 1 个,每层索引的结点数: n2,n4,n8...,8,4,2
  • 原始链表大小为 n,每 3 个结点抽 1 个,每层索引的结点数: n3,n9,n27...,9,3,1
  • 空间复杂度是 O(n)
跳跃表的构成以下是个典型的跳跃表例子:
看完Redis源码还不理解跳跃表吗?

文章插图
 
从图中可以看到,跳跃表主要由以下部分构成:
  • 表头(head):负责维护跳跃表的节点指针 。
  • 跳跃表节点:保存着元素值,以及多个层 。
  • 层:保存着指向其他元素的指针 。高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次 。
  • 表尾:全部由 NULL 组成,表示跳跃表的末尾 。
Redis 跳跃表的实现Redis 的跳跃表由 redis.h/zskiplistNode 和 redis.h/zskiplist 两个结构定义,其中 zskiplistNode 结构用于表示跳跃表节点,而 zskiplist 结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针,等等 。
看完Redis源码还不理解跳跃表吗?

文章插图
 
上图展示了一个跳跃表示例,位于图片最左边的是 zskiplist 结构,该结构包含以下属性:
  • header :指向跳跃表的表头节点 。
  • tail :指向跳跃表的表尾节点 。
  • level :记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内) 。
  • length :记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内) 。
位于 zskiplist 结构右方的是四个 zskiplistNode 结构,该结构包含以下属性:
  • 层(level):节点中用 L1 、 L2 、 L3 等字样标记节点的各个层, L1 代表第一层, L2 代表第二层,以此类推 。每个层都带有两个属性:前进指针和跨度 。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离 。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度 。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行 。
  • 后退(backward)指针:节点中用 BW 字样标记节点的后退指针,它指向位于当前节点的前一个节点 。后退指针在程序从表尾向表头遍历时使用 。
  • 分值(score):各个节点中的 1.0 、 2.0 和 3.0 是节点所保存的分值 。在跳跃表中,节点按各自所保存的分值从小到大排列 。
  • 成员对象(obj):各个节点中的 o1 、 o2 和 o3 是节点所保存的成员对象 。
注意:表头节点和其他节点的构造是一样的: 表头节点也有后退指针、分值和成员对象,不过表头节点的这些属性都不会被用到,所以图中省略了这些部分,只显示了表头节点的各个层 。
跳跃表节点跳跃表节点的实现由 redis.h/zskiplistNode 结构定义:
typedef struct zskiplistNode {    // 后退指针    struct zskiplistNode *backward;    // 分值    double score;    // 成员对象    robj *obj;    // 层    struct zskiplistLevel {        // 前进指针        struct zskiplistNode *forward;        // 跨度        unsigned int span;    } level[];} zskiplistNode;层跳跃表节点的 level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快 。
每次创建一个新跳跃表节点的时候,程序都根据幂次定律 (power law,越大的数出现的概率越小) 随机生成一个介于 1 和 32 之间的值作为 level 数组的大小,这个大小就是层的“高度” 。


推荐阅读