首页 > 数据库 >Redis 跳表

Redis 跳表

时间:2023-05-21 20:33:21浏览次数:55  
标签:struct 元素 Redis 跳表 层数 节点 指针

参考

小林:https://xiaolincoding.com/redis/data_struct/data_struct.html#%E8%B7%B3%E8%A1%A8

cmu: https://www.cs.cmu.edu/~ckingsf/bioinfo-lectures/skiplists.pdf

wiki:https://en.wikipedia.org/wiki/Skip_list

 

 

跳表

这种数据结构是由William Pugh(音译为威廉·普)发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。

https://dl.acm.org/doi/pdf/10.1145/78973.78977

链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。跳表是在链表基础上改进过来的(相当于给链表加上索引),实现了一种「多层」的有序链表,这样的好处是能快读定位数据。跳表的优势是能支持平均 O(logN) 复杂度的节点查找。

查找例1 

查找例2

完美的跳表:

  • key 按序排列
  • 一共有 O(logn) 层
  • 每层包含的元素数量是它下面那一层的 1/2
  • 头节点(哨兵)节点在每一层(如上面的 31 节点 1、2、3、4 层都在)
  • 节点大小可变,可以包含从 1 到 O(logn) 个 next 指针(如 31 节点包含4个next 指针,第一层的只包含一个指针)
  • 叫做跳表是因为更高层的 list 让你可以跳过很多元素

如果要 插入 或 删除,同时保持 每层包含的元素数量是它下面那一层的 1/2 的完美结构,那么每次插入或删除都可能要重排列整个跳表。

像完美二叉搜索树一样,完美跳跃列表过于结构化,无法支持高效更新。因此,解决方法:

  • 放松 每层包含的元素数量是它下面那一层的 1/2 的条件
  • 取而代之地,设计一种结构,期望 每层 1/2 的元素被提升到一个新的层级
  • 跳表是一种随机化的数据结构:同样的 插入/删除的顺序可能会产生不同的结构,取决于随机结果

随机化

  • 随机化就意味着,允许不严格满足 每层包含的元素数量是它下面那一层的 1/2 的条件
  • 每个节点提升到下一个更高层次的概率为 1/2 (不一定是 1/2,可以是一个概率 p)
    • 期望 1/2 的节点在 层级1
    • 期望 1/4 的节点在 层级2
    • 期望 1/8 的节点在 层级3
    • ......
  • 期望概率会使所有节点的分布接近完美跳表

所以可以在每插入一个新元素时,随机生成这个元素所在的层数,使每一层出现的概率满足上面的要求——幂次定律:越大的数(层数)出现的概率越小

例如 p 取 0.5,每次随机生成 [0,1) 之间的一个随机数,如果这个随机数小于 0.5 并且小于限制的最高层数,那么层数+1;再次生成一个随机数,如果这个随机数小于 0.5 并且小于限制的最高层数,那么层数+1;直到生成的随机数大于 0.5 或者层数已经超过最大层数限制,那么停止继续。

每个节点的平均层数为 1/(1-p)(也是每个节点的指针个数)

  • 节点层数至少为1,大于1的节点层数满足一个概率分布。
  • 节点层数恰好等于1的概率为 (p^0)(1-p)
  • 节点层数恰好等于2的概率为 (p^1)(1-p)
  • 节点层数恰好等于3的概率为 (p^2)(1-p) 【前两层随机到 p 向上升级,第三层随机到 1-p ,不再向上升级,停止】
  • 节点层数恰好等于4的概率为 (p^3)(1-p)
  • 依次递推节点层数恰好等于K的概率为 [p^(k-1)](1-p)

因此如果我们要求节点的平均层数,那么也就转换成了求概率分布的期望问题了

每个节点的层数期望:

  1*(p^0)(1-p) + 2*(p^1)(1-p) + 3*(p^2)(1-p)  + ... + k* [p^(k-1)](1-p)

= (1-p)*[1*p^0 + 2*p^1 + 3*p^2 + ... + k*p^(k-1)]

= (1-p) * (1/(1-p)^2) 

= 1/(1-p)

插入例

 

删除例

 

跳表相对于平衡树的优势

  1. 内存占用上来比较,跳表比平衡树更灵活一些。平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针(1/(1-p)),比平衡树更有优势。
  2. 做 ZRANGE 或 ZREVRANGE 范围查找 的时候,跳表比平衡树操作要简单。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序(中序遍历是有序的)继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历(最底层包含所有元素且有序)就可以实现。
  3. 算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。

 

Redis 跳表

Redis 只有 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。

zset 结构体 

有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的 范围查询(跳表),也能进行高效 单点查询 (哈希表)

Zset 对象在执行数据插入或是数据更新的过程中,会依次在跳表和哈希表中插入或更新相应的数据,从而保证了跳表和哈希表中记录的信息一致。

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

「跳表」结构体

定义哪个跳表节点是头节点

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

「跳表节点」的数据结构

  • 「元素」:sds 类型的 ele 变量
  • 「元素的权重」:double 类型的 score 变量
  • 「节点的 level 数组」:该节点每个层级指向下个节点的指针。leve[0] 表示第一层的 forward 指针,leve[1] 表示第二层的 forward 的指针。每个 level 元素,还包含 「跨度」span,跨度是用来记录两个节点之间的距离。
  • 「后向指针」:指向(第一层的)前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。
typedef struct zskiplistNode {
    //Zset 对象的元素值
    sds ele;
    //元素权重值
    double score;
    //后向指针
    struct zskiplistNode *backward;
  
    //节点的level数组,保存每层上的前向指针和跨度
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

跨度 实际上是为了 计算这个节点在跳表中的排位。具体怎么做的呢?因为跳表中的节点都是按序排列的,那么计算某个节点排位的时候,从头节点点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位。

Redis 里面随机化的概率 p 取 1/4 , 即 每个节点提升到下一个更高层次的概率为 1/4

 

标签:struct,元素,Redis,跳表,层数,节点,指针
From: https://www.cnblogs.com/suBlog/p/17419111.html

相关文章

  • 用redis实现支持优先级的消息队列
    用redis实现支持优先级的消息队列 为什么需要消息队列系统中引入消息队列机制是对系统一个非常大的改善。例如一个web系统中,用户做了某项操作后需要发送邮件通知到用户邮箱中。你可以使用同步方式让用户等待邮件发送完成后反馈给用户,但是这样可能会因为网络的不确定性造成用......
  • 关于Redis的应用
    写这篇文章主要是在开发www.ximalaya.com的feed(登录首页看到的好友动态,未登录是看不到的)模块使用Redis的一些经验。(www.ximalaya.com是音频为传播介质的SNS网站,喜欢的同学不妨用一用,也有APP的,还是一款非常不错的产品。音乐,相声,有声小说等等一网打尽)。关于Feed本身的讨论以后再专......
  • 在java中使用lua脚本操作redis
    前言众所周知,redis可以执行lua脚本,至于为什么要用lua脚本来操作redis,自行百度咯开始Bean类packagecn.daenx.myadmin.common.config.redis;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.......
  • redis-cli 使用lua脚本笔记
    前言众所周知,redis可以执行lua脚本,至于为什么要用lua脚本来操作redis,自行百度咯先来讲一下最简单的方式,关于如何在javaspringboot里用lua脚本,请查看我另一篇文章:https://www.cnblogs.com/daen/p/17418024.html更为详细的资料请参考以下文章https://blog.csdn.net/jiayibingd......
  • golang操作redis
    首先,基于docker查看redis镜像dockerps-a然后执行dockerrun-p6379:6379-dredis:latestredis-server,将端口映射到本机最后执行dockerexec-ti13e638ea1036redis-cli-h0.0.0.0-p6379执行链接操作......
  • redis学习3linux--黑马
    持久化RDBsavebgsave工作原理bgsave->发送指令到redis,redis返回Backgroundsavingstarted给客户端,然后调用fork函数生成子进程,子进程创建rdb文件,成功后返回消息给redis,可通过日志文件查看bgsave命令时针对save阻塞问题的优化。Reids内部所有涉及到RDB操作都采用bgsave的方......
  • Redis笔记(六):Redis订阅发布
    CommandsSUBSCRIBEchannel[channel...]PUBLISHchannelmessageUNSUBSCRIBE[channel[channel...]]PSUBSCRIBEpattern[pattern...]正则订阅PUBSUBsubcommand[argument[argument...]]查看订阅与发布系统状态PUNSUBSCRIBE[pattern[pattern...]]退订所有给......
  • 【≅Redis】BitMap类型介绍
    BitMap(2.2版新增)Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。由于bit是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计......
  • Redis笔记(四):Java集成和配置
    JedisJedis是Redis官方提供的Java客户端,用于在Java应用程序中连接、操作Redis,它提供了与Redis通信的API,简化了Java开发者与Redis的交互流程。JedisGithubReadme:https://github.com/redis/jedis#getting-startedSpringBoot在SpringBoot2.x之后,原来使用的jedis被替换成了lettc......
  • springboot整合redis
     前言Redis是一款key-value存储结构的内存级NoSQL数据库支持多种数据存储格式支持持久化支持集群Redis下载(Windows版)https://github.com/tporadowski/redis/releasesRedis安装与启动(Windows版)Windows解压安装或一键式安装服务端启动命令redis-server.exe......