Redis数据结构之跳跃表

一、简介

咱们先不谈Redis,来看一下跳表。node

1.一、业务场景

场景来自小灰的算法之旅,咱们须要作一个拍卖行系统,用来查阅和出售游戏中的道具,相似于魔兽世界中的拍卖行那样,还有如下需求:python

  1. 拍卖行拍卖的商品须要支持四种排序方式,分别是:按价格、按等级、按剩余时间、按出售者ID排序,排序查询要尽量地快。
  2. 还要支持输入道具名称的精确查询和不输入名称的全量查询。

这样的业务场景所须要的数据结构该如何设计呢?拍卖行商品列表是线性的,最容易表达线性结构的是数组和链表。假如用有序数组,虽然查找的时候可使用二分法(时间复杂度O(logN)),可是插入的时间复杂度是O(N),整体时间复杂度是O(N);而若是要使用有序链表,虽然插入的时间复杂度是O(1),可是查找的时间复杂度是O(N),整体仍是O(N)。mysql

那有没有一种数据结构,查找时,有二分法的效率,插入时有链表的简单呢?有的,就是 跳表redis

1.二、skiplist

skiplist,即跳表,又称跳跃表,也是一种数据结构,用于解决算法问题中的查找问题。算法

通常问题中的查找分为两大类,一种是基于各类平衡术,时间复杂度为O(logN),一种是基于哈希表,时间复杂度O(1)。可是skiplist比较特殊,没有在这里面sql

二、跳表

2.一、跳表简介

跳表也是链表的一种,是在链表的基础上发展出来的,咱们都知道,链表的插入和删除只须要改动指针就好了,时间复杂度是O(1),可是插入和删除必然伴随着查找,而查找须要从头/尾遍历,时间复杂度为O(N),以下图所示是一个有序链表(最左侧的灰色表示一个空的头节点)(图片来自网络,如下同):mongodb

image-20201110214951860

链表中,每一个节点都指向下一个节点,想要访问下下个节点,必然要通过下个节点,即没法跳过节点访问,假设,如今要查找22,咱们要前后查找 3->7->11->19->22,须要五次查找。数组

可是若是咱们可以实现跳过一些节点访问,就能够提升查找效率了,因此对链表进行一些修改,以下图:网络

image-20201110215429303

咱们每一个一个节点,都会保存指向下下个节点的指针,这样咱们就能跳过某个节点进行访问,这样,咱们实际上是构造了两个链表,新的链表以后原来链表的一半。数据结构

咱们姑且称原链表为第一层,新链表为第二层,第二层是在第一层的基础上隔一个取一个。假设,如今仍是要查找22,咱们先从第二层查找,从7开始,7小于22,再日后,19小于22,再日后,26大于22,因此从节点19转到第一层,找到了22,前后查找 7->19->26->22,只须要四次查找。

以此类推,若是再提取一层链表,查找效率岂不是更高,以下图:

image-20201110220408220

如今,又多了第三层链表,第三层是在第二层的基础上隔一个取一个,假设如今仍是要查找22,咱们先从第三层开始查找,从19开始,19小于22,再日后,发现是空的,则转到第二层,19后面的26大于22,转到第一层,19后面的就是22,前后查找 19->26>22,只须要三次查找。

由上例可见,在查找时,跳过多个节点,能够大大提升查找效率,skiplist 就是基于此原理。

上面的例子中,每一层的节点个数都是下一层的一半,这种查找的过程有点相似二分法,查找的时间复杂度是O(logN),可是例子中的多层链表有一个致命的缺陷,就是一旦有节点插入或者删除,就会破坏这种上下层链表节点个数是2:1的结构,若是想要继续维持,则须要在插入或者删除节点以后,对后面的全部节点进行一次从新调整,这样一来,插入/删除的时间复杂度就变成了O(N)。

2.二、跳表层级之间的关系

如上所述,跳表为了解决插入和删除节点时形成的后续节点从新调整的问题,引入了随机层数的作法。相邻层数之间的节点个数再也不是严格的2:1的结构,而是为每一个新插入的节点赋予一个随机的层数。下图展现了如何经过一步步的插入操做从而造成一个跳表:

image-20201110220408220

每个节点的层数都是随机算法得出的,插入一个新的节点不会影响其余节点的层数,所以,插入操做只须要修改插入节点先后的指针便可,避免了对后续节点的从新调整。这是跳表的一个很重要的特性,也是跳表性能明显因为平衡树的缘由,由于平衡树在失去平衡以后也须要进行平衡调整。

上图最后的跳表中,咱们须要查找节点22,则遍历到的节点依次是:7->37->19->22,可见,这种随机层数的跳表的查找时可能没有2:1结构的效率,可是却解决了插入/删除节点的问题。

2.三、跳表的复杂度

跳表搜索的时间复杂度平均 O(logN),最坏O(N),空间复杂度O(2N),即O(N)

三、Redis中的跳表

在理解 Redis 的跳跃表以前,咱们先回忆一下 Redis 的有序集合(sorted set)操做

  • 不重复但有序的字符串元素集合;
  • 每一个元素均关联一个double类型的score,Redis 根据score进行从小到大排序;
  • score能够重复,重复的按照插入顺序进行排序;

示例以下:

redis 127.0.0.1:6379> ZADD runoobkey 1 redis
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 2 mongodb
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql
(integer) 1
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql
(integer) 0
redis 127.0.0.1:6379> ZADD runoobkey 4 mysql
(integer) 0
redis 127.0.0.1:6379> ZRANGE runoobkey 0 10 WITHSCORES

"redis"
"1"
"mongodb"
"2"
"mysql"
"4"

这个是 Redis 中的有序列表的基本操做,咱们答题能够看出,在有序列表中,有一个浮点数做为 score, 当对应一个值,能够根据 score 精确查找和范围查找,且效率很高

Redis 里面的这种操做的底层实现就是跳表。

上面理解了跳表,再去看 Redis 中的跳表就轻松多了,跳表的实如今 Redis 源码目录下 redis.h 文件中

3.一、zskiplistNode

zskiplistNode 表示跳表的一个节点,声明以下:

typedef struct zskiplistNode {
    robj *obj;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

robj 类型是 Redis 中用C语言实现一种集合数据结构,它能够表示 string、hash、list、set 和 zset 五种数据类型,这里不作详细说明,在跳表节点中,这个类型的指针表示节点的成员对象

score 表示分值,用于排序和范围查找

level 是一个柔性数组,它表示节点的层级,每层都有一个前进指针 forward,用于指向相同层级指向表尾方向的下一个节点,而 span 则表示当前节点在当前层级中距离下一个节点的跨度,即两个节点之间的距离。

初看上去,很容易觉得跨度和遍历节点有关,实际并非,遍历操做只用前进指针就够了,跨度是用来计算排位(rank)的:在查找某个节点的过程当中,沿途访问过的全部层的跨度累计起来,就是目标节点在跳表中的排位。

下图中,查找成员o3,只经历了一层,排位为3

image-20201110232105792

在 Redis 中,每一个节点的层级都是根据幂次定律(power law,越大的树出现的几率越小)随机生成的,它是1~32之间的一个数,做为level数组的大小,即高度

下图分别展现了三个高度为一、三、5层的节点

image-20201110231806856

backward 是一个后退指针,每一个节点都有一个,指向当前节点的表头方向的下一个节点,用于从表尾进行遍历

3.二、zskiplist

zskiplist 表示一个跳表,声明以下:

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

headertail 指针分别指向表头和表尾节点

length 记录了节点数量

level 记录了全部节点中层级最高的节点的层级,表头节点的层高不计算在内

下图是一个跳表的示例,最左侧是一个 zskiplist 结构,其右侧是四个 zskiplistNode 节点,从左向右分别有32层、4层、2层、5层。每一个节点向右的指针即前进指针 forward, BW 则表示后退指针 backward,每一个节点依据节点的分值 score 进行排列

image-20201110232246288

相关文章
相关标签/搜索