Redis数据结构之跳表

news2024/12/28 8:28:55

跳表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。其核心思想就是通过建立多级索引来实现空间换时间。
在Redis中,使用跳表作为Zset的一种底层实现之一,这也是跳表在Redis中的唯一使用场景。

跳表的实现

跳表由zskiplistNode和zskiplist两个结构定义。其中zskiplistNode表示跳跃表的节点,zskiplist则表示跳跃表节点的相关信息。
跳表

zskiplistNode

typedef struct zskiplistNode {
    sds ele; // 元素值
    double score; // 分值
    struct zskiplistNode *backward; // 后退指针
    struct zskiplistLevel { // 各层信息
        struct zskiplistNode *forward; // 该层前向指针
        unsigned long span; // 该层的跨度
    } level[];
} zskiplistNode;

前进节点

每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。

跨度

记录两个节点之间的距离。跨度是用来计算rank的,在查找某个节点的过程中,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的rank。

后退指针

用于表示表尾向表头方向的访问节点,后退节点每次只能后退至前一个节点。

zskiplistList

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头、尾指针
    unsigned long length; // 跳表长度
    int level; // 跳表层数
} zskiplist;

header

指向跳跃表的表头节点。

tail

指向跳跃表的表尾节点。

level

记录当前跳表的长度,即跳表包含节点的数量。

跳表的操作

新建跳表

/* Create a new skiplist. */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    zsl = zmalloc(sizeof(*zsl));
    zsl->level = 1;
    zsl->length = 0;
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    return zsl;
}
  • 首先分配内存
  • level设置为1,length设置为0,后退指针设置为null,尾节点设置为null
  • 头指针指向一个高度为32的节点
  • 为头指针的前进节点设置为null,span设置为0

插入节点

/* Insert a new node in the skiplist. Assumes the element does not already
 * exist (up to the caller to enforce that). The skiplist takes ownership
 * of the passed SDS string 'ele'. */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // update记录每层的应该指向新增节点的节点
    unsigned long rank[ZSKIPLIST_MAXLEVEL]; // rank记录每层需要更新的span值
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; // 最高层rank为0,非最高层rank初始化为上一层的rank值
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||
                    (x->level[i].forward->score == score &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        { // 对分值&元素遍历对比 直至找到合适的位置
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /* we assume the element is not already inside, since we allow duplicated
     * scores, reinserting the same element should never happen since the
     * caller of zslInsert() should test in the hash table if the element is
     * already inside or not. */
    level = zslRandomLevel(); // 根据幂次定律生成响应的层数
    if (level > zsl->level) { // 对于高出现有的层数,依次遍历,更新rank、后置节点和跨度
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    x = zslCreateNode(level,score,ele); // 生成节点
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward; // 针对每一层实现节点的插入。新插入的节点x的forward指向update
        update[i]->level[i].forward = x; // update的forward指向x节点

        /* update span covered by update[i] as x is inserted here */
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); // 对x节点更新span
        update[i]->level[i].span = (rank[0] - rank[i]) + 1; // 对update节点更新span
    }

    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) { // 所有高出的层级更新span++
        update[i]->level[i].span++;
    }

    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x; // 更新zsl的尾节点
    zsl->length++; // 更新zsl的长度
    return x;
}

更新节点分值

/* Update the score of an element inside the sorted set skiplist.
 * Note that the element must exist and must match 'score'.
 * This function does not update the score in the hash table side, the
 * caller should take care of it.
 *
 * Note that this function attempts to just update the node, in case after
 * the score update, the node would be exactly at the same position.
 * Otherwise the skiplist is modified by removing and re-adding a new
 * element, which is more costly.
 *
 * The function returns the updated element skiplist node pointer. */
zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // 记录需要更新节点在每一层的位置
    int i;

    /* We need to seek to element to update to start: this is useful anyway,
     * we'll have to update or remove it. */
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
                (x->level[i].forward->score < curscore ||
                    (x->level[i].forward->score == curscore &&
                     sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;
    }

    /* Jump to our element: note that this function assumes that the
     * element with the matching score exists. */
    x = x->level[0].forward;
    serverAssert(x && curscore == x->score && sdscmp(x->ele,ele) == 0);

    /* If the node, after the score update, would be still exactly
     * at the same position, we can just update the score without
     * actually removing and re-inserting the element in the skiplist. */
    if ((x->backward == NULL || x->backward->score < newscore) &&
        (x->level[0].forward == NULL || x->level[0].forward->score > newscore))
    { // 如果针对最后一个节点的更新分值变大或者对第一个节点的更新分值减小,可以直接更新分值即可,无需移动节点
        x->score = newscore;
        return x;
    }

    /* No way to reuse the old node: we need to remove and insert a new
     * one at a different place. */
    zslDeleteNode(zsl, x, update); // 首先将旧分值节点删除
    zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele); // 为新分值新建一个新的节点,并插入
    /* We reused the old node x->ele SDS string, free the node now
     * since zslInsert created a new one. */
    x->ele = NULL;
    zslFreeNode(x);
    return newnode;
}

根据排名获取节点

/* Finds an element by its rank from start node. The rank argument needs to be 1-based. */
zskiplistNode *zslGetElementByRankFromNode(zskiplistNode *start_node, int start_level, unsigned long rank) {
    zskiplistNode *x;
    unsigned long traversed = 0;
    int i;

    x = start_node;
    for (i = start_level; i >= 0; i--) { // 从最高层开始查找,如果上层找到直接return,否则进入下一层
        while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
        {
            traversed += x->level[i].span; // 每一层更新已查找的排名
            x = x->level[i].forward;
        }
        if (traversed == rank) { // 对比排名
            return x;
        }
    }
    return NULL; // 最终不存在
}

删除节点

/* Internal function used by zslDelete, zslDeleteRangeByScore and
 * zslDeleteRangeByRank. */
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    for (i = 0; i < zsl->level; i++) {
        if (update[i]->level[i].forward == x) {
            update[i]->level[i].span += x->level[i].span - 1;
            update[i]->level[i].forward = x->level[i].forward;
        } else {
            update[i]->level[i].span -= 1;
        }
    }
    if (x->level[0].forward) {
        x->level[0].forward->backward = x->backward;
    } else {
        zsl->tail = x->backward;
    }
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    zsl->length--;
}

Zset获取元素的分值

Zset跳表和字典
Zset除了使用zskiplist来实现之外,结构中还使用字典为有序集合创建了一个成员到分值的映射。字典中的每个键值对都保存了一个集合元素,其中键保存了元素的成员,值保存了元素的分值。通过字典可以O(1)的实现查看某个元素的分值。zscore命令就是根据这一特性实现的。
另外需要主要的是,虽然zset结构同时使用跳表和字典来保存有序集合元素,但是两种结构通过指针共享相同元素的成员和分值,所以同时使用跳表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1276968.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

西南科技大学(数据结构A)期末自测练习五

一、选择题&#xff08;每空 1 分&#xff0c;共 5 分&#xff09; 1、下面关于图的叙述中&#xff0c;正确的是&#xff08; &#xff09;。 (1)&#xff0e;回路是简单路径 (2)&#xff0e;存稀疏矩阵&#xff0c;用邻接矩阵比邻接表更省空间 (3)&#xff0e;若有像图中存在…

Seaborn可视化图形绘制_Python数据分析与可视化

Seaborn可视化图形绘制 频次直方图、KDE和密度图矩阵图分面频次直方图条形图折线图 Seaborn的主要思想是用高级命令为统计数据探索和统计模型拟合创建各种图形&#xff0c;下面将介绍一些Seaborn中的数据集和图形类型。 虽然所有这些图形都可以用Matplotlib命令实现&#xff08…

MySQL的系统信息函数

系统信息函数让你更好的使用MySQL数据库 1、version()函数 查看MySQL系统版本信息号 select version();2、connection_id()函数 查看当前登入用户的连接次数 直接调用CONNECTION_ID()函数--不需任何参数--就可以看到当下连接MySQL服务器的连接次数&#xff0c;不同时间段该…

深度学习第4天:感知机模型

☁️主页 Nowl &#x1f525;专栏《机器学习实战》 《机器学习》 &#x1f4d1;君子坐而论道&#xff0c;少年起而行之 ​ 文章目录 感知机模型介绍 神经网络搭建感知机 结构 准备训练数据 感知机的损失函数与优化方法 测试结果 完整代码 多层感知机 结语 感知机模…

【C语言】【字符串函数的模拟实现】strcpy,strcat,strcmp,strncpy,strncat,strstr

1.strcpy char* strcpy(char*destination,const char* source)源字符串必须以’\0’结尾会将原字符串中的‘\0’拷贝到目标字符串中目标空间必须足够大&#xff0c;能放得下源字符串 模拟实现&#xff1a; char *my_strcpy(char* des,const char *sour) {char* retdes;asser…

MinkowskiEngine安装

本人配置&#xff1a; cuda10.1, gcc7.5.0, g7.5.0 gcc --version # 查看gcc版本代码 g --version #查看g版本代码安装步骤&#xff1a; pip install ninja # 安装依赖git clone https://github.com/NVIDIA/MinkowskiEngine.git # 下载到本地 cd MinkowskiEngine # 进入…

大三上oracle数据库期末复习

1、创建表空间 2、创建用户 3、用户授权 oracle数据库逻辑存储结构&#xff1a; 1、表空间&#xff08;最大的逻辑存储单元&#xff09; 创建表空间 2、段 3、盘区&#xff08;最小的磁盘空间分配单元&#xff09; 4、数据块&#xff08;最小的数据读写单元&#xff09; 用…

Java核心知识点整理大全26-笔记

目录 27. Storm 7.1.1. 概念 27.1.1. 集群架构 27.1.1.1. Nimbus&#xff08;master-代码分发给 Supervisor&#xff09; 27.1.1.2. Supervisor&#xff08;slave-管理 Worker 进程的启动和终止&#xff09; 27.1.1.3. Worker&#xff08;具体处理组件逻辑的进程&#xff…

周报:浅谈对豆瓣网页实战的注意事项

制作整体网页时HTML代码和CSS代码的常用处理方法&#xff1a; 分开HTML代码和CSS代码&#xff0c;专门制作一个CSS文件专门来放置css代码&#xff0c;css文件里一般有作者样式(XXX.css)和通用样式(common.css)。这样会使代码更易维护&#xff0c;且整齐美观。 写代码前的注意…

HT81298 集成免滤波器调制D类音频功放

HT81298是一款内置升压的立体声D类音频功率放大器&#xff0c;HT81298内部集成免滤波器调制技术&#xff0c; 能够直接驱动扬声器&#xff0c;内置的关断功能使待机 电流Z小化&#xff0c;还集成了输出端过流保护、片内 过温保护、输入电源欠压异常保护、升压电压 过压保护等功…

TextToSpeech类学习和简单封装

TextToSpeech类简单学习封装 前言一、TTS是什么&#xff1f;二、TextToSpeech简单使用1.官方介绍2.简单使用 三、TextToSpeech简单封装总结 前言 业务涉及到对接TTS相关&#xff0c;所以简单学习下如何使用。 一、TTS是什么&#xff1f; TextToSpeech简称为TTS&#xff0c;即…

在 SQL Server 中备份和恢复数据库的最佳方法

在SQL Server中&#xff0c;创建备份和执行还原操作对于确保数据完整性、灾难恢复和数据库维护至关重要。以下是备份和恢复过程的概述&#xff1a; 方法 1. 使用 SQL Server Management Studio (SSMS) 备份和还原数据库 按照 SSMS 步骤备份 SQL 数据库 打开 SSMS 并连接到您…

【数据结构和算法】找出叠涂元素

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、题目描述 二、题解 三、代码 四、复杂度分析 前言 这是力扣的2661题&#xff0c;难度为中等&#xff0c;解题方案有很多种&…

LeNet对MNIST 数据集中的图像进行分类--keras实现

我们将训练一个卷积神经网络来对 MNIST 数据库中的图像进行分类&#xff0c;可以与前面所提到的CNN实现对比CNN对 MNIST 数据库中的图像进行分类-CSDN博客 加载 MNIST 数据库 MNIST 是机器学习领域最著名的数据集之一。 它有 70,000 张手写数字图像 - 下载非常简单 - 图像尺…

规则引擎专题---2、开源规则引擎对比

开源规则引擎 开源的规则引擎整体分为下面几类&#xff1a; 通过界面配置的成熟规则引擎&#xff0c;这种规则引擎相对来说就比较重&#xff0c;但功能全&#xff0c;比较出名的有:drools, urule。 基于jvm脚本语言&#xff0c;互联网公司会觉得drools太重了&#xff0c;然后…

用100ask 6ull配合 飞凌 elf1的教程进行学习的记录

启动方式 百问网 elf1: 固件 emmc-otg 串口 网络 改eth0, 网线接在右边的网口eth2上

spring boot mybatis TypeHandler 看源码如何初始化及调用

目录 概述使用TypeHandler使用方式在 select | update | insert 中加入 配置文件中指定 源码分析配置文件指定Mapper 执行query如何转换 结束 概述 阅读此文 可以达到 spring boot mybatis TypeHandler 源码如何初始化及如何调用的。 spring boot 版本为 2.7.17&#xff0c;my…

触控板绘画工具Inklet mac功能介绍

Inklet mac是一款触控板绘画工具&#xff0c;把你的触控板变成画画的板子&#xff0c;意思是&#xff0c;你点在触控板的哪里&#xff0c;鼠标就会出现载相应的地方。例如&#xff0c;但你把手指移动到触控盘左下角&#xff0c;那么鼠标也会出现在左下角&#xff0c;对于用户而…

【已解决】Cannot find project Scala library 2.11.8 for module XXX

问题描述 在 flink 示例程序调试过程中&#xff0c;reload project 报错 Cannot find project Scala library 2.11.8 for module HbasePrint 报错如下图所示&#xff1a; 问题解决 经过搜索&#xff0c;初步判定是 pom 文件中 Scala 版本配置和项目中实际使用的版本不一致导…

11.29 知识回顾(视图层、模板层)

一、视图层 1.1 响应对象 响应---》本质都是 HttpResponse -HttpResponse---》字符串 -render----》放个模板---》模板渲染是在后端完成 -js代码是在客户端浏览器里执行的 -模板语法是在后端执行的 -redirect----》重定向 -字符串参数不是…