详解Redis数据结构之跳跃表

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;
  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数据结构跳跃表内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Redis中5种数据结构的使用场景介绍

    一.redis 数据结构使用场景 原来看过 redisbook 这本书,对 redis 的基本功能都已经熟悉了,从上周开始看 redis 的源码.目前目标是吃透 redis 的数据结构.我们都知道,在 redis 中一共有5种数据结构,那每种数据结构的使用场景都是什么呢? String--字符串 Hash--字典 List--列表 Set--集合 Sorted Set--有序集合 下面我们就来简单说明一下它们各自的使用场景: 1. String--字符串 String 数据结构是简单的 key-

  • redis内部数据结构之SDS简单动态字符串详解

    前言 reids 没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组)而是构建了一种名为简单动态字符串的抽象类型,并为redis的默认字符串表示,因为C字符串不能满足redis对字符串的安全性.效率以及功能方面的需求 1.SDS 定义 在C语言中,字符串是以'\0'字符结尾(NULL结束符)的字符数组来存储的,通常表达为字符指针的形式(char *).它不允许字节0出现在字符串中间,因此,它不能用来存储任意的二进制数据. sds的类型定义 typedef char *sds; 每个sds

  • redis中的数据结构和编码详解

    redis中的数据结构和编码:     背景: 1>redis在内部使用redisObject结构体来定义存储的值对象. 2>每种类型都有至少两种内部编码,Redis会根据当前值的类型和长度来决定使用哪种编码实现. 3>编码类型转换在Redis写入数据时自动完成,这个转换过程是不可逆的,转换规则只能从小内存编码向大内存编码转换.     源码: 值对象redisObject: typedef struct redisObject {                 unsigned ty

  • 详解redis数据结构之压缩列表

     详解redis数据结构之压缩列表 redis使用压缩列表作为列表键和哈希键的底层实现之一.当一个列表键只包含少量的列表项,并且每个列表项都是由小整数值或者是短字符串组成,那么redis就会使用压缩列表存储列表项:同理,当一个哈希表包含的键值对都是由小整数值或者是短字符串组成,并且存储的键值对数目不多时,redis也会使用压缩列表来存储哈希表.以下是压缩列表存储结构: zlbytes长度为4个字节,记录了整个压缩列表所占用的字节数 zltail长度为4个字节,记录了压缩列表起始位置到压缩列表尾节

  • 详解redis数据结构之sds

    详解redis数据结构之sds 字符串在redis中使用非常广泛,在redis中,所有的数据都保存在字典(Map)中,而字典的键就是字符串类型,并且对于很大一部分字典值数据也是又字符串组成的.以下是sds的具体存储结构: 从图中可以看出,sds的属性有三个:len.free和buf数组.这里len字段是用来保存sds字符串中所包含字符数目的,free字段则是用来保存buf数组中空余的部分的长度的,而buf数组则是实际用来保存字符串的.比如如下结构保存了"Hello World!"这个字

  • 详解Redis数据结构之跳跃表

    1.简介 我们先不谈Redis,来看一下跳表. 1.1.业务场景 场景来自小灰的算法之旅,我们需要做一个拍卖行系统,用来查阅和出售游戏中的道具,类似于魔兽世界中的拍卖行那样,还有以下需求: 拍卖行拍卖的商品需要支持四种排序方式,分别是:按价格.按等级.按剩余时间.按出售者ID排序,排序查询要尽可能地快.还要支持输入道具名称的精确查询和不输入名称的全量查询. 这样的业务场景所需要的数据结构该如何设计呢?拍卖行商品列表是线性的,最容易表达线性结构的是数组和链表.假如用有序数组,虽然查找的时候可以使用

  • 详解Python数据结构与算法中的顺序表

    目录 0. 学习目标 1. 线性表的顺序存储结构 1.1 顺序表基本概念 1.2 顺序表的优缺点 1.3 动态顺序表 2. 顺序表的实现 2.1 顺序表的初始化 2.2 获取顺序表长度 2.3 读取指定位置元素 2.4 查找指定元素 2.5 在指定位置插入新元素 2.6 删除指定位置元素 2.7 其它一些有用的操作 3. 顺序表应用 3.1 顺序表应用示例 3.2 利用顺序表基本操作实现复杂操作 0. 学习目标 线性表在计算机中的表示可以采用多种方法,采用不同存储方法的线性表也有着不同的名称和特

  • 详解Redis数据类型实现原理

    目录 1. 对象的类型与编码 ① type属性 ② encoding 属性和 *prt 指针 2. 字符串对象 ① 编码 ② 编码的转换 3. 列表对象 ① 编码 ② 编码转换 4. 哈希对象 ① 编码 ② 编码转换 5. 集合对象 ① 编码 ② 编码转换 6. 有序集合对象 ① 编码 ② 编码转换 7. 五大数据类型的应用场景 1. 对象的类型与编码 Redis使用前面说的五大数据类型来表示键和值,每次在Redis数据库中创建一个键值对时,至少会创建两个对象,一个是键对象,一个是值对象,而Re

  • 详解Redis中的List类型

    本系列将和大家分享Redis分布式缓存,本章主要简单介绍下Redis中的List类型,以及如何使用Redis解决博客数据分页.生产者消费者模型和发布订阅等问题. Redis List的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用这个数据结构. List类型主要用于队列和栈,先进先出,后进先出等. 存储形式:key--LinkList<value> 首先先给大家Show一波Redis中与List类型相

  • 详解Redis基本命令与使用场景

    Redis和Memcached对比 其中有一个比较重要的区别是关于其提供的数据结构区别 Memcached 在其数据结构中仅使用字符串和整数.因此,您保存的所有内容都可以是字符串或整数.它很复杂,因为对于整数,您可以做的唯一数据操作是添加或减去它们.如果需要保存数组或对象,则必须先将它们序列化然后保存.要阅读它们,您需要取消序列化. Redis 具有更强大的数据结构,它不仅可以处理字符串整数,还可以处理二进制安全字符串,二进制安全字符串列表,二进制安全字符串集和有序集. 关于Redis的数据结构

  • 详解Redis 数据类型

    Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合). String(字符串) string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value. string 类型是二进制安全的.意思是 redis 的 string 可以包含任何数据.比如jpg图片或者序列化的对象. string 类型是 Redis 最基本的数据类型,string 类

  • 详解Redis中key的命名规范和值的命名规范

    数据库中得热点数据key命名惯例 表名:主键名:主键值:字段名 例如 user:id:0001:name 例如 user:id:0002:name 例如 order:id:s2002:price 上面的key对应的值则可以是 存放的方式 key value 优点 单独的key:value形式 order:id:s2002:price 2000 方便简单的操作,例如incr自增或自减 json格式 user:id:0001 {id:0001,name:"张三"} 方便一次性存和取数据,但

  • 详解redis缓存与数据库一致性问题解决

    数据库与缓存读写模式策略 写完数据库后是否需要马上更新缓存还是直接删除缓存? (1).如果写数据库的值与更新到缓存值是一样的,不需要经过任何的计算,可以马上更新缓存,但是如果对于那种写数据频繁而读数据少的场景并不合适这种解决方案,因为也许还没有查询就被删除或修改了,这样会浪费时间和资源 (2).如果写数据库的值与更新缓存的值不一致,写入缓存中的数据需要经过几个表的关联计算后得到的结果插入缓存中,那就没有必要马上更新缓存,只有删除缓存即可,等到查询的时候在去把计算后得到的结果插入到缓存中即可. 所

随机推荐