Redis底层数据结构之有序集合的跳跃表

Redis 当一个有序集合包含的元素数量多,又或者有序集合中的元素的成员是比较长的字符串时,Redis就会使用跳跃表作为有序集合键的底层实现

跳跃表的基本思想

首先我们看一个普通的链表结构:
在这里插入图片描述

这个链表中,如果要搜索一个数,需要从头到尾比较每个元素是否匹配,直到找到匹配的数为止,即时间复杂度是 O(n)。同理,插入一个数并保持链表有序,需要先找到合适的插入位置,再执行插入,总计也是 O(n)的时间。

但假如我们每相邻两个节点之间就增加一个指针,让指针指向下一个节点,如下图:
在这里插入图片描述
如上图,我们新创建一个链表,它包含的元素为前一个链表的偶数个元素。这样在搜索一个元素时,我们先在上层链表进行搜索,当元素未找到时再到下层链表中搜索。例如搜索数字 19 时的路径如下图:
在这里插入图片描述
先在上层中搜索,到达节点 17 时发现下一个节点为 21,已经大于 19,于是转到下一层搜索,找到的目标数字 19。

利用同样的方式,我们可以在新产生的链表上,继续为每两个相邻的节点增加一个指针,从而产生第三层链表。同理,我们可以不断地增加层数,来减少搜索的时间:
在这里插入图片描述
在上面的 4 层链表中搜索 25,在最上层搜索时就可以直接跳过 21 之前的所有节点,因此十分高效。

更进一步的跳跃表

可以想象,当链表足够长,上面的多层链表结构可以帮助我们跳过很多下层节点,从而加快查找的效率。

但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的 2:1 的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点 重新进行调整,这会让时间复杂度重新蜕化成 O(n)。删除数据也有同样的问题。

因此跳表(skip list)表示,我们就不强制要求 1:2 了,一个节点要不要被索引,建几层的索引,都在节点插入时由抛硬币决定。当然,虽然索引的节点、索引的层数是随机的,为了保证搜索的效率,要大致保证每层的节点数目与上节的结构相当。下面是一个随机生成的跳跃表:
在这里插入图片描述

每一个节点的层数是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整,这就降低了插入操作的复杂度。

关于随机层数(抛硬币算法的redis实现)

对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数,源码在 t_zset.c/zslRandomLevel(void) 中被定义

int zslRandomLevel(void) { 
    int level = 1; 
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) 
        level += 1; 
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; 
}

直观上期望的目标是 50% 的概率被分配到 Level 1,25% 的概率被分配到 Level 2,12.5% 的概率被分配到 Level 3,每一层的晋升率都是 50%。

小结

1.各种搜索结构提高效率的方式都是通过空间换时间得到的。
2.跳跃表最终形成的结构和搜索树很相似。
3.跳跃表通过随机的方式来决定新插入节点来决定索引的层数。
4.跳跃表搜索的时间复杂度是 O(logn),插入/删除也是。


二次解释

下图是一个有序链表:
在这里插入图片描述

如果将有序链表的部分节点分层,每层都是一个有序链表。查找时优先从最高层开始向后查找,当到达某节点时,如果next节点值大于要查找的值或next指针指向NULL,则从当前节点下降一层继续向后找,这种“跳过一些节点”的方式就是跳表查找的思路。如下图所示是一个有序分层链

除第0层外的其他层所有节点均有一个down指针指向下层节点
第0层包含了所有数据
其他层的每个节点一定都是从其下一层中选出来的

在这里插入图片描述
这种链表加多级索引的结构就是跳表。

查找过程
上图中,如果要查找值为51的节点,步骤如下:
(1)从最高层即第2层开始,1节点比51小,继续向后找
(2)21节点比51小,且21节点的next指针为null,跳到第1层查找
(3)41节点比51节点小,继续向后查找
(4)61节点比51节点大,跳到第1层查找
(5)51节点等于51,找到返回
跳表的思想就是:将有序集合的部分节点分层,由最上层一次向后查找,如果本层的next节点大于要查找的节点,或next值为null,则向下一层查找,直到找到或返回空。

Redis有序集合中的跳表
下图就是一个Redis有序集合的跳表结构:
在这里插入图片描述
跳表节点的结构体定义如下:

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;
ele:存储字符串类型的数据,如学生姓名
score:用于存储排序的分值,如学生成绩
backward:指向当前节点的前一个节点,头结点的backward值为null,用于从后往前遍历跳跃表
level:柔性数组,forward指向该节点对应的下一层节点
span:forward指向的节点与本节点之间的元素个数,span值越大,跳过的节点数越多

跳表结构:

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;
header:指向跳表的头节点,头结点的level为64层,是Redis跳表的最大层数
tail:指向跳表的尾节点
length:存储跳表节点个数,不包括头结点
level:跳跃表的高度,即最大层数,不包括头结点

跳表的插入
【注:图片来自 https://blog.csdn.net/universsky2015/article/details/102728114】
在这里插入图片描述
上图展示了跳表插入的一个插入过程,跳表的插入过程也需要像平衡树一样维护“平衡性”,如果所有插入的数据都在11到37之间,则跳表的查询时间复杂度很快就会退化成在单链表上的查询时间复杂度了。在Redis有序集合中维护跳表“平衡性”采用的是产生一个随机数k,将要添加的节点添加至第1层至第k层,上图元素区域被拉长就是表明该元素同时也插入到了其他高层上。即zskiplistNode的level层数是随机产生的一个值。

时间复杂度分析

如果链表里有n个结点,会有多少级索引呢?

假设平均每隔两个结点会抽出一个结点作为上一级索引的结点,那第一级索引的结点个数大约就是n/2,第二级索引的结点个数大约就是n/4,第三级索引的结点个数大约就是n/8,依次类推,也就是说,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那第k级索引结点的个数就是n/(2^ k)。

假设索引有h级,最高级的索引有2个结点,则n/(2^ h)=2得到h=log2n-1,所以包含原始数据这一层,跳表的高度就是log2n。假设每层都要遍历m个节点,则在跳表中查找一个数据的时间复杂度就是O(m*logn)。那m的值是多少呢?
按照上面的说法“假设平均每隔两个结点会抽出一个结点作为上一级索引的结点”,可以得到下面这张图:
在这里插入图片描述
假设我们要查找的数据是x。在第k级索引中遍历到y结点之后,发现x大于y,小于后面的结点z,所以我们通过y的down指针,从第k级索引下降到第k-1级索引。在第k-1级索引中,y和z之间只有3个结点(包含y和z),所以,我们在K-1级索引中最多只需要遍历3个结点,依次类推,每一级索引都最多只需要遍历3个结点。

因此,跳表查找的平均时间复杂度就是O(logn),和二分查找的平均时间复杂度一样。

空间复杂度分析
按照分析时间复杂度的假设: “假设平均每隔两个结点会抽出一个结点作为上一级索引的结点”,原始数据有n个,则第1层索引有n/2个节点、第2层索引有n/4个节点…、第k层有n/(2^ k)个节点,依次累加,得到n-2。所以跳表的空间复杂度是O(n),即n个数据用跳表的结构存储,还需要额外n个节点的存储空间。

小灰的跳跃表解释

Redis是一个开源的内存数据库,它使用了多种数据结构来存储不同类型的数据。下面是几种常见的Redis底层数据结构的详解: 1. 字符串(String):字符串是Redis中最基本的数据结构。它可以存储任意类型的数据,包括数字、文本等。字符串在Redis中以字节数组的形式存储,可以通过键访问和修改。 2. 列表(List):列表是一个有序的字符串集合,可以在列表的两端进行插入、删除和获取操作。Redis使用双向链表来实现列表数据结构,它支持快速插入和删除操作。 3. 哈希(Hash):哈希是一种键值对的集合。在Redis中,哈希可以存储多个字段和对应的值,类似于关联数组或者字典。哈希在内部使用哈希表来实现,可以快速查找和修改字段值。 4. 集合(Set):集合是一组唯一且无序的字符串集合Redis使用哈希表来实现集合数据结构,它支持添加、删除和判断元素是否存在等操作。 5. 有序集合(Sorted Set):有序集合是一组唯一且有序的字符串集合。在Redis中,每个元素都会关联一个分数,通过分数可以对元素进行排序。有序集合的实现使用了跳跃表和哈希表两种数据结构,它支持添加、删除、修改和范围查询等操作。 这些数据结构底层实现都是高效的,并且支持丰富的操作。Redis数据结构灵活性较高,能够满足不同类型的数据存储需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值