Redis底层数据结构之深入理解跳表(1)

news2025/6/6 22:35:43

        在上一篇文章中我们详细的介绍了一下Redis中跳表的结构以及为什么Redis要引入跳表而不是平衡树或红黑树。这篇文章我们就来详细梳理一下跳表的增加、搜索和删除步骤。

SkipList的初始化

        跳表初始化时,将每一层链表的头尾节点创建出来并使用集合将头尾节点进行存储,头尾节点的层数随机指定,且头尾节点的层数就代表当前跳表的层数。初始化之后,跳表的结构如下所示:

        跳表初始化的相关代码如下所示:

public class SkiplistNode {
    public int data;
    public SkiplistNode next;
    public SkiplistNode down;
    public int level;

    public SkiplistNode(int data, int level) {
        this.data = data;
        this.level = level;
    }
}

public LinkedList<SkiplistNode> headNodes;
public LinkedList<SkiplistNode> tailNodes;

public int curLevel;

public Random random;

public Skiplist() {
    random = new Random();
    headNodes = new LinkedList<>();//headNodes用于存储每一层的头节点
    tailNodes = new LinkedList<>();//tailNodes用于存储每一层的尾节点
    curLevel = getRandomLevel();//初始化跳表时,跳表的层数随机指定

    //指定了跳表的初始的随机层数后,就需要将每一层的头节点和尾节点创建出来并构建好关系
    SkiplistNode head = new SkiplistNode(Integer.MIN_VALUE, 0);
    SkiplistNode tail = new SkiplistNode(Integer.MAX_VALUE, 0);

    for (int i = 0; i <= curLevel; i++) {
        head.next = tail;
        headNodes.addFirst(head);
        tailNodes.addFirst(tail);

        SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
        SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
        headNew.down = head;
        tailNew.down = tail;

        head = headNew;
        tail = tailNew;
    }
}

SkipList的添加方法

        每一个元素添加到跳表中时,首先需要随机指定这个元素在跳表中的层数,如果随机指定的层数大于了原有跳表的层数,那么在将新元素加入到跳表之前,还要对跳表的层数进行扩大,而跳表层数的扩大就是将头尾节点的层数进行扩大。下面给出需要扩大跳表层数的依次添加过程。

        初始状态,跳表的层数为2,如下图所示:

        现在要向跳表中添加元素80,并随机指定了其层数为3,大于了当前跳表的层数2,此时需要先将跳表的层数扩充到3,如下图所示:

        最后将元素80插入到跳表中,插入时从顶层向下逐层插入,如下图所示:

        跳表的添加方法的相关代码如下所示:

public void add(int num) {

    int level = getRandomLevel();//获取本次添加的值的层数

    //如果本次添加的值的层数大于当前跳表的层数,则需要在添加当前值前先将跳表层数扩充
    if (level > curLevel) {
        expanLevel(level - curLevel);
    }

    //curNode表示num值在当前层对应的节点
    SkiplistNode curNode = new SkiplistNode(num, level);
    //preNode表示curNode在当前层的前一个节点
    SkiplistNode preNode = headNodes.get(curLevel - level);
    for (int i = 0; i <= level; i++) {

        //从当前层的head节点开始向后遍历,直到找到一个preNode
        //使得preNode.data < num <= preNode.next.data
        while (preNode.next.data < num) {
            preNode = preNode.next;
        }

        //将curNode插入到preNode和preNode.next中间
        curNode.next = preNode.next;
        preNode.next = curNode;

        //如果当前并不是0层,则继续向下层添加节点
        if (curNode.level > 0) {
            SkiplistNode downNode = new SkiplistNode(num, curNode.level - 1);
            curNode.down = downNode;//curNode指向下一层的节点
            curNode = downNode;//curNode向下移动一层
        }

        //preNode向下移动一层
        preNode = preNode.down;
    }
}

private void expanLevel(int expanCount) {
    SkiplistNode head = headNodes.getFirst();//取出头节点中level最高的
    SkiplistNode tail = tailNodes.getFirst();//取出尾节点中level最高的


    //循环扩充level,直到与新加入节点的level相等
    for (int i = 0; i < expanCount; i++) {
        SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
        SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
        headNew.down = head;
        tailNew.down = tail;

        head = headNew;
        tail = tailNew;

        headNodes.addFirst(head);
        tailNodes.addFirst(tail);
    }
}

SkipList的搜索方法

        在跳表中搜索一个元素时,需要从顶层开始,逐层向下搜索。当目标值大于当前节点的后一个节点值时,需要在本层链表上继续向后搜索;当目标值大于当前节点值,小于当前节点的后一个节点值时,向下移动一层,并从下层链表的同节点位置向后搜索;当目标值等于当前节点值时,搜索结束并返回。具体流程如下图所示:

        跳表的搜索代码如下所示:

public boolean search(int target) {
    SkiplistNode curNode = headNodes.getFirst();//从顶层开始寻找,curNode表示当前遍历到的节点
    while (curNode != null) {
        if (curNode.next.data == target) {
            return true;//从顶层开始寻找,curNode表示当前遍历到的节点
        } else if (curNode.next.data > target) {
            //curNode的后一节点值大于target,说明目标节点在curNode和curNode.next之间
            //此时需要向下层寻找
            curNode = curNode.down;
        } else {
            //curNode的后一节点值小于target,说明目标节点在curNode的后一节点的后面
            //此时在本层继续向后寻找
            curNode = curNode.next;
        }
    }
    return false;
}

SkipList的删除方法

        当在跳表中需要删除一个元素时,则需要将这个元素在所有层的节点全部删除。首先按照跳表的搜索方式找到要删除的节点,如果可以找到,此时搜索到的节点对应要删除节点的最高层;从待删除节点的最高层往下,将每一层的待删除节点都删除掉,删除方式就是让待删除节点的前一节点直接指向待删除节点的后一节点。具体流程如下图所示:

        跳表的删除的相关代码如下所示:

public boolean erase(int num) {
    SkiplistNode curNode = headNodes.getFirst();//拿到level最高的头结点开始查询
    while (curNode != null) {
        if (curNode.next.data == num) {
            SkiplistNode preDeleteNode = curNode;//preDeleteNode表示待删除节点的前一节点
            while (true) {
                //删除当前层的待删除节点,就是让待删除节点的前一节点指向待删除节点的后一节点
                preDeleteNode.next = curNode.next.next;

                //当前层删除完后,需要继续删除下一层的待删除节点
                //这里让preDeleteNode向下移动一层
                //向下移动一层后,preDeleteNode就不一定是待删除节点的前一节点了
                preDeleteNode = preDeleteNode.down;

                //如果preDeleteNode为null,说明已经将底层的待删除节点删除了
                //此时就结束删除流程,并返回true
                if (preDeleteNode == null) {
                    return true;
                }

                //preDeleteNode向下移动一层后,需要继续从当前位置向后遍历
                //直到找到一个preDeleteNode,使得preDeleteNode.next的值等于目标值
                //此时preDeleteNode就又变成了待删除节点的前一节点
                while (preDeleteNode.next.data != num) {
                    preDeleteNode = preDeleteNode.next;
                }
            }
        } else if (curNode.next.data > num) {
            curNode = curNode.down;
        } else {
            curNode = curNode.next;
        }
    }
    return false;
}

        关于跳表的增删查就讲到这里,大家有什么问题或者勘误可以在评论区留言,笔者看到都会回复的。        

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

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

相关文章

20250529-C#知识:继承、密封类、密封方法、重写

C#知识&#xff1a;继承、密封类、密封方法、重写 继承是面向对象的三大特性之一&#xff0c;通过继承能够减少重复代码的编写&#xff0c;有助于提升开发效率。 1、继承 C#不同于C&#xff0c;只支持单继承当子类出现与父类同名的成员时&#xff0c;父类成员被隐藏&#xff0…

从0到1,带你走进Flink的世界

目录 一、Flink 是什么&#xff1f; 二、Flink 能做什么&#xff1f; 三、Flink 架构全景概览 3.1 分层架构剖析 3.2 核心组件解析 四、Flink 的核心概念 4.1 数据流与数据集 4.2 转换操作 4.3 窗口 4.4 时间语义 4.5 状态与检查点 五、Flink 安装与快速上手 5.1 …

springboot @value

#springboot value value 可以读取 yaml 中 的数据

Dify-5:Web 前端架构

本文档提供了 Dify Web 前端架构的技术概述&#xff0c;包括核心组件、结构和关键技术。它解释了前端如何组织、组件如何通信以及国际化功能如何实现。 技术栈 Dify 的 Web 前端基于现代 JavaScript 技术栈构建&#xff1a; 框架&#xff1a;Next.js&#xff08;基于 React …

深度学习赋能图像识别:技术、应用与展望

论文&#xff1a; 一、引言​ 1.1 研究背景与意义​ 在当今数字化时代&#xff0c;图像作为信息的重要载体&#xff0c;广泛存在于各个领域。图像识别技术旨在让计算机理解和识别图像内容&#xff0c;将图像中的对象、场景、行为等信息转化为计算机能够处理的符号或数据 &am…

八N皇后问题

1 问题的提出 在8X8格的国际象棋上摆放八个皇后&#xff0c;使其不能互相攻击&#xff0c;即任意两个皇后都不能处于同一行、同一列或同一斜线上&#xff0c;问有多少种摆法 我们的任务就是用MATLAB进行求解 2 数学模型的构建 首先我们分析题目就是 任意两个皇后都不能处于…

TMS320F28388D使用sysconfig配置IPC

第1章 配置IPC底层代码 使用IPC的动机&#xff1a; 我计划我的项目中要使用RS485&#xff0c;CANFD通信和EtherCAT通信&#xff0c;由于通信种类较多&#xff0c;而对于电机控制来说大部分数据都是重复的&#xff0c;并且有些数据可以很久才改变一次&#xff0c;所以我计划使…

Qwen与Llama分词器核心差异解析

Qwen和 Llama 词映射(分词器)的区别及通用词映射逻辑 一、Qwen 与 Llama 词映射(分词器)区别 维度Qwen 分词器Llama 分词器技术基础基于字节级别字节对编码(BBPE),以 cl100k 为基础词库,扩充中文字词、多语言词汇基于 BPE,但依赖 SentencePiece 单字模型,核心为英文优…

华为云Flexus+DeepSeek征文 | 基于ModelArts Studio 与 Cline 快速构建AI编程助手

目录 一、前言 二、ModelArts Studio&#xff08;MaaS&#xff09;介绍与应用场景 2.1ModelArts Studio&#xff08;MaaS&#xff09;介绍 2.2 ModelArts Studio&#xff08;MaaS&#xff09;使用场景 2.3 开通MaaS服务 2.4 开通DeepSeek-V3商用服务 三、Cline简介和安装 3.1 C…

pikachu靶场通关笔记11 XSS关卡07-XSS之关键字过滤绕过(三种方法渗透)

目录 一、源码分析 1、进入靶场 2、代码审计 3、攻击思路 二、渗透实战 1、探测过滤信息 2、注入Payload1 3、注入Payload2 4、注入Payload3 本系列为通过《pikachu靶场通关笔记》的XSS关卡(共10关&#xff09;渗透集合&#xff0c;通过对XSS关卡源码的代码审计找到安…

碰一碰发视频-源码系统开发技术分享

#碰一碰营销系统# #碰一碰系统# #碰一碰发视频# 架构设计哲学&#xff1a;近场通信的优雅平衡 一、核心通信技术选型 1. 双模协同传输引擎 技术协议栈延迟控制适用场景NFCISO 14443-A<100ms精准触发场景BLE 5.0GATT Profile300-500ms中距传输场景 工程决策依据&…

C++学习过程分享

空指针&#xff1a;int *p NULL; 空指针&#xff1a;指针变量指向内存中编号为0的空间&#xff1b;用途&#xff1a;初始化指针变量注意&#xff1a;空指针指向的内存不允许访问注意&#xff1a;内存编号为0-255为系统占用空间&#xff0c;不允许用户访问 野指针&#xff1a;…

C语言 — 动态内存管理

目录 1.malloc和free函数1.1 malloc函数1.2 free函数1.3 malloc函数的使用 2.calloc函数2.1 calloc函数2.2 calloc函数的使用 3.realloc函数3.1 realloc函数3.2 realloc函数的使用 4.动态内存管理笔试题4.1 笔试题&#xff08;1&#xff09;4.2 笔试题&#xff08;2&#xff09…

《TCP/IP 详解 卷1:协议》第5章:Internet协议

IPv4和IPv6头部 IP是TCP/IP协议族中的核心协议。所有TCP、UDP、ICMP和IGMP 数据都通过IP数据报传输。IP提供了一种尽力而为、无连接的数据报交付服务。 IP头部字段 IPv4 头部通常为 20 字节&#xff08;无选项时&#xff09;&#xff0c;而 IPv6 头部固定为 40 字节。IPv6 不…

学习STC51单片机26(芯片为STC89C52RCRC)

每日一言 真正的强者&#xff0c;不是没有眼泪&#xff0c;而是含着泪依然奔跑。 硬件&#xff1a;4G模块 这个是接线原理&#xff0c;我们也只要知道这个4根线的连接就好了&#xff0c;我们也是连接到USB转TTL的模块上 要插卡哈......... 随后我们下载一个叫做亿佰特的调试助…

Nginx详解(三):ngx_http_rewrite_module模块核心指令详解

概要&#xff1a; 在 Nginx 的众多功能模块中&#xff0c;ngx_http_rewrite_module是实现请求动态处理的核心组件&#xff0c;它通过一系列指令实现 URI 重写、条件判断、响应返回等功能。本文将以 CentOS 7.9 环境为例&#xff08;主机名www.a.com&#xff0c;IP 172.25.0.10…

【笔记】在 MSYS2(MINGW64)中正确安装 Poetry 的指南

#工作记录 在 MSYS2&#xff08;MINGW64&#xff09;中正确安装 Poetry 的指南 一、背景说明 在 MSYS2&#xff08;MINGW64&#xff09;环境中&#xff0c;即使已经安装了 pip&#xff0c;也不建议直接使用 pip install poetry 来安装 Poetry。 这是因为 MSYS2 使用自己的包…

IDEA项目推送到远程仓库

打开IDEA——>VCS——>Creat Git 选择项目 push提交到本地 创建远程仓库 复制地址 定义远程仓库 推送 推送成功

【后端架构师的发展路线】

后端架构师的发展路线是从基础开发到技术领导的系统性进阶过程&#xff0c;需融合技术深度、架构思维和业务洞察力。以下是基于行业实践的职业发展路径和关键能力模型&#xff1a; 一、职业发展阶梯‌ 初级工程师&#xff08;1-3年&#xff09;‌ 核心能力‌&#xff1a;掌…

matlab/simulink TLC语法基础练习实例

一、基本语法测试方法 1.新建一个脚本&#xff0c;保存扩展名为tlc,本例中是tst.tlc&#xff0c;设置当前工作路径为保存的tlc文件路径&#xff0c;在tlc文件里面输入下面的代码&#xff0c;然后保存&#xff1a; %warning test 2.在MATLAB的命令窗口输入&#xff1a; tlc …