详解Redis数据结构之跳跃表
⽬录
1、简介
1.1、业务场景
1.2、skiplist
2、跳表
2.1、跳表简介
2.2、跳表层级之间的关系
2.3、跳表的复杂度
3、Redis中的跳表
3.1、zskiplistNode
3.2、zskiplist
1、简介
我们先不谈Redis,来看⼀下跳表。
1.1、业务场景
场景来⾃⼩灰的算法之旅,我们需要做⼀个拍卖⾏系统,⽤来查阅和出售游戏中的道具,类似于魔兽世界中的拍卖⾏那样,还有以下需求:
拍卖⾏拍卖的商品需要⽀持四种排序⽅式,分别是:按价格、按等级、按剩余时间、按出售者ID排序,排序查询要尽可能地快。还要⽀持输⼊道具名称的精确查询和不输⼊名称的全量查询。
这样的业务场景所需要的数据结构该如何设计呢?拍卖⾏商品列表是线性的,最容易表达线性结构的是数组和链表。假如⽤有序数组,虽然查的时候可以使⽤⼆分法(时间复杂度O(logN)),但是插⼊的时间复杂度是O(N),总体时间复杂度是O(N);⽽如果要使⽤有序链表,虽然插⼊的时间复杂度是O(1),但是查的时间复杂度是O(N),总体还是O(N)。
那有没有⼀种数据结构,查时,有⼆分法的效率,插⼊时有链表的简单呢?有的,就是跳表。
1.2、skiplist
skiplist,即跳表,⼜称跳跃表,也是⼀种数据结构,⽤于解决算法问题中的查问题。
⼀般问题中的查分为两⼤类,⼀种是基于各种平衡术,时间复杂度为O(logN),⼀种是基于哈希表,时间复杂度O(1)。但是skiplist⽐较特殊,没有在这⾥⾯
2、跳表
2.1、跳表简介
跳表也是链表的⼀种,是在链表的基础上发展出来的,我们都知道,链表的插⼊和删除只需要改动指针就⾏了,时间复杂度是O(1),但是插⼊和删除必然伴随着查,⽽查需要从头/尾遍历,时间复杂度为O(N),如下图所⽰是⼀个有序链表(最左侧的灰⾊表⽰⼀个空的头节点)(图⽚来⾃⽹络,以下同):
链表中,每个节点都指向下⼀个节点,想要访问下下个节点,必然要经过下个节点,即⽆法跳过节点
访问,假设,现在要查22,我们要先后查 3->7->11->19->22,需要五次查。
但是如果我们能够实现跳过⼀些节点访问,就可以提⾼查效率了,所以对链表进⾏⼀些修改,如下图:
我们每个⼀个节点,都会保存指向下下个节点的指针,这样我们就能跳过某个节点进⾏访问,这样,我们其实是构造了两个链表,新的链表之后原来链表的⼀半。
我们姑且称原链表为第⼀层,新链表为第⼆层,第⼆层是在第⼀层的基础上隔⼀个取⼀个。假设,现在还是要查22,我们先从第⼆层查,从7开始,7⼩于22,再往后,19⼩于22,再往后,26⼤于22,所以从节点19转到第⼀层,到了22,先后查 7->19->26->22,只需要四次查。
以此类推,如果再提取⼀层链表,查效率岂不是更⾼,如下图:
现在,⼜多了第三层链表,第三层是在第⼆层的基础上隔⼀个取⼀个,假设现在还是要查22,我们先从第三层开始查,从19开始,19⼩于22,再往后,发现是空的,则转到第⼆层,19后⾯的26⼤于22,转到第⼀层,19后⾯的就是22,先后查 19->26>22,只需要三次查。
由上例可见,在查时,跳过多个节点,可以⼤⼤提⾼查效率,skiplist 就是基于此原理。
上⾯的例⼦中,每⼀层的节点个数都是下⼀层的⼀半,这种查的过程有点类似⼆分法,查的时间复杂度是O(logN),但是例⼦中的多层链表有⼀个致命的缺
陷,就是⼀旦有节点插⼊或者删除,就会破坏这种上下层链表节点个数是2:1的结构,如果想要继续维持,则需要在插⼊或者删除节点之后,对后⾯的所有节点进⾏⼀次重新调整,这样⼀来,插⼊/删除的时间复杂度就变成了O(N)。
2.2、跳表层级之间的关系
如上所述,跳表为了解决插⼊和删除节点时造成的后续节点重新调整的问题,引⼊了随机层数的做法。相邻层数之间的节点个数不再是严格的2:1的结构,⽽是为每个新插⼊的节点赋予⼀个随机的层数。下图展⽰了如何通过⼀步步的插⼊操作从⽽形成⼀个跳表:
每⼀个节点的层数都是随机算法得出的,插⼊⼀个新的节点不会影响其他节点的层数,因此,插⼊操作只需要修改插⼊节点前后的指针即可,避免了对后续节点的重新调整。这是跳表的⼀个很重要的特性,也是跳表性能明显由于平衡树的原因,因为平衡树在失去平衡之后也需要进⾏平衡调整。
上图最后的跳表中,我们需要查节点22,则遍历到的节点依次是:7->37->19->22,可见,这种随机层数的跳表的查时可能没有2:1结构的效率,但是却解决了插⼊/删除节点的问题。
2.3、跳表的复杂度
跳表搜索的时间复杂度平均 O(logN),最坏O(N),空间复杂度O(2N),即O(N)
3、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.1、zskiplistNode
zskiplistNode 表⽰跳表的⼀个节点,声明如下:
typedef struct zskiplistNode {
robj *obj;redis支持的数据结构
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
在 Redis 中,每个节点的层级都是根据幂次定律(power law,越⼤的树出现的概率越⼩)随机⽣成的,它是1~32之间的⼀个数,作为level数组的⼤⼩,即⾼度下图分别展⽰了三个⾼度为1、3、5层的节点
backward 是⼀个后退指针,每个节点都有⼀个,指向当前节点的表头⽅向的下⼀个节点,⽤于从表尾进⾏遍历
3.2、zskiplist
zskiplist 表⽰⼀个跳表,声明如下:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
header 和 tail 指针分别指向表头和表尾节点
length 记录了节点数量
level 记录了所有节点中层级最⾼的节点的层级,表头节点的层⾼不计算在内
下图是⼀个跳表的⽰例,最左侧是⼀个 zskiplist 结构,其右侧是四个 zskiplistNode 节点,从左向右分别有32层、4层、2层、5层。每个节点向右的指针即前进指针 forward, BW 则表⽰后退指针 backward,每个节点依据节点的分值 score 进⾏排列
到此这篇关于Redis数据结构中的跳跃表的⽂章就介绍到这了,更多相关Redis数据结构跳跃表内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论