什么是跳表,Java如何实现跳表?

news2025/1/19 3:42:04

1. 问题引入,相较于有序链表我们为什么需要跳表?

1.1 首先我们需要了解什么是有序链表

如图:

每个链表存在一个指向下一节点的指针,如果我们要对其任一节点进行增删改,都需要先使用迭代器进行查询,找到指定节点进行修改,复杂度较高。

1.2 因此我们可以对有序列表进行分层

 

如果next节点大于我们查找的值或者指向null那么就需要从当前节点下降一层,继续向后查找,如此一来可以极大提高查找效率。

2. 跳表性质

  1. 跳表由很多层组成。
  2. 跳表有一个头节点(header),头结点中有一个64层结构,每层结构包含指向本层下个节点的指针,只想本层下个节点中间跨越的节点个数称为本层跨度(span)。
  3. 除头节点外,层数最多的节点的层高为跳表的高度(level)。
  4. 每层都是一个有序链表,数据递增。
  5. 除头节点外,,一个元素若在上层出现那么一定会在下层出现。
  6. 每层最后一定指向null,表示本层有序链表结束。
  7. 跳表存在一个tail指针,指向跳表最后一个节点。
  8. 最底层的有序链表包含所有节点,最底层节点个数为跳表长度(不包括头节点)
  9. 每个节点都包含了一个后退指针,头节点和第一个结点指向NULL;其他节点指向最底层前一个结点。

3. 跳表的创建实现

1. 创建跳表节点

查看redis源码可知我们需要先创建一个节点,跳跃表节点存在如下参数:

  1. ele: 用于存储字符串类型数据
  2. score: 用于存储排序的分值
  3. backward:后退指针
  4. level:柔性数组,包含:
    • forward: 指向本层下一个节点,前进指针
    • span: 跨度,用于增删改找寻修改节点位置

我们在java中可以使用Object 对象obj来存储ele这个对象

 /**
     * 定义节点内容
     */
    private static class ZSkipListNode implements SkipList.Node{
        // 存储数据对象
        Object obj;

        // 用于存储排序的分值
        double score;

        // 后退指针
        ZSkipListNode backward;

        @Override
        public double getScore() {
            return score;
        }

        @Override
        public Object getObject() {
            return obj;
        }

2. 创建跳跃表结构

       在c语言底层跳表结构是通过一个叫 zskiplist 结构体来实现的,用来管理节点。

包含以下属性:

  1. header:指向跳表头节点。
  2. tail:指向尾节点(在最底层)
  3. length:跳表长度
  4. level:跳表高度

java实现如下:

     /**
         * 1. level 是一个柔性数组,每个节点的数组长度不一样,
         *    在生成跳跃表节点时,随机生成一个 1 ~ 64 的值,值越大出现概率越低。
         * 2. level数组包含两个元素:
         *      a. forward 前进指针
         *          指向本层下一个节点,最终指向 null
         *      b. span 跨度
         *          指向的节点与本节点之间的元素个数
         */
        static class ZSkipListLevel{
            // 前进指针
            ZSkipListNode forward;

            // 跨度
            int span;
        }

        // 定义层
        ZSkipListLevel[] level;

        // 初始化构造器
        public ZSkipListNode(int level, double score, Object obj){
            // 设置层数大小
            ZSkipListLevel[] skipListLevels = new ZSkipListLevel[level];
            this.level = skipListLevels;

            // 设置属性
            this.score = score;
            this.obj = obj;
        }
   /**
     * 1. 定义表头节点和尾节点
     * 2. 注意头节点中不存储仍和 member和 score值,obj为 null,score为 0; 也不计入跳表总长度。
     */
    ZSkipListNode header;
    ZSkipListNode tail;

    // 定义表节点数量(注意:不包括头节点)
    long length;

    // 表示跳表高度
    int level;

    // 创建跳表
    public ZSkipList() {
        // 设置高度和起始层数

        this.length = 0;
        this.level = 1;
        // 创建初始化具有64层的头节点
        this.header = new ZSkipListNode(ZSKIPLIST_MAXLEVEL, 0, null);
        // 头节点每层都有一个level软列表
        // 头节点每层forward指向null
        // 头节点层数是0
        for (int i = 0; i < ZSKIPLIST_MAXLEVEL; i++){
            this.header.level[i] = new ZSkipListNode.ZSkipListLevel();
            this.header.level[i].forward = null;
            this.header.level[i].span = 0;
        }
        // 回调节点和尾节点都为null
        this.header.backward = null;
        this.tail = null;
    }

4. 增删改查方法

然后我们就需要进行增删改查,但是由于查是增删改基础所以就不单独展示,而删除只是对增改方法进行一定的简化改写,所以下面只展示增改加方法。

根据redis源码,我们可以看到增改方法由两个很重要的数组分别是:

  1. update[]:用于记录需要被跟新/插入节点前一个几点。
  2. rank[]:用于记录当前层从header头节点到update[i]节点的跨度。

所以可以简单通过java来实现一下找寻节点的操作:

/**
     * 1. 为了找到要更新的节点,我们需要以下两个长度为64的数组来辅助操作
     *      a. update[]: 插入节点时,需要更新被插入节点每层的前一个节点。
     *          由于每层更新结点不一样,所以需要将每层需要更新的节点记录在update[i]中
     *      b. rank[]: 记录当前层从header节点到update[i]节点所经历的步长,
     *          更新 update[i]的span和设置新插入节点的span时使用
     * 2.
     * @param score
     * @param obj
     * @return
     */
    @Override
    public Node insert(double score, Object obj) {
        ZSkipListNode[] update = new ZSkipListNode[ZSKIPLIST_MAXLEVEL];
        int[] rank = new int[ZSKIPLIST_MAXLEVEL];
        // 定义一个节点
        ZSkipListNode x;
        int i, level;

        // 在各层查找节点插入位置
        x = this.header;
        for (i = this.level - 1; i>= 0; i--){
            // 如果 i 不是 zsl->level-1 层
            // 那么 i 层的起始 rank 值为 i+1 层的rank值
            // 各层rank的rank值一层层累积
            // 最终 rank[0] 的值加一就是新节点的前置节点的排位
            // rank[0] 会成为计算span和rank的值基础
            rank[i] = i == (this.level - 1) ? 0 : rank[i + 1];

            // 沿着前进指针遍历跳跃表
            // 当前分支小于目标分值或者分值相同但是对象字典小于目标
            while (x.level[i].forward != null &&
                    (x.level[i].forward.score < score ||
                            (x.level[i].forward.score == score &&
                                    compareStringObjects(x.level[i].forward.obj, obj) < 0))){
                // 记录跨越节点数
                rank[i] += x.level[i].span;

                // 移动到下一节点指针
                x = x.level[i].forward;
            }
            // 记录将要和新节点相连的节点
            // 新节点在i层,指向该节点第i层前进节点
            update[i] = x;
        }

1. 调整跳表高度

由于我们插入新节点,高度是随机的,所以我们需要新增高度,并且进行一些记录参数的调整:

 level = randomLevel();

        // 如果新节点的层数比表中其他节点的层数都要大
        // 那么初始化header节点中未使用的这层,并将他记录到 update 数组中
        // 将来也指向新节点
        if (level > this.level){
            // 初始化未使用层
            for (i = this.level; i < level; i++){
                rank[i] = 0;
                update[i] = this.header;
                update[i].level[i].span = (int) this.length;
            }
            this.level = level;
        }

2. 插入节点

由于我们已经做完了准备工作,接下来就可以对节点进行一个简单的插入就🆗了:

x = new ZSkipListNode(level, score, obj);
        // 将前面记录的指针指向新节点,并做对应设置
        for (i = 0; i < level; i++){
            x.level[i] = new ZSkipListNode.ZSkipListLevel();
            //设置新节点的 forward 指针
            x.level[i].forward = update[i].level[i].forward;

            // 将沿途记录各个节点的 forward 指针指向新节点
            update[i].level[i].forward = x;

            // 计算新节点跨越节点数量
            x.level[i].span = update[i].level[i].span - (rank[0] - rank[i]);

            // 更新新节点插入之后,沿途节点的span值
            // 其中的 +1 计算的是新节点
            update[i].level[i].span = (rank[0] - rank[i]) + 1;
        }
        // 由于新增了节点,所以未接触节点的跨度也要增加1,这些节点直接从表头指向新节点
        for (i = level; i < this.level; i++){
            update[i].level[i].span++;
        }

3. 接下来我们在调整后退指针就完成了增改方法:

// 调整新节点的后退指针
        x.backward = (update[0] == this.header) ? null : update[0];
        // 调整第1层
        if (x.level[0].forward != null){
            x.level[0].forward.backward = x;
        }else {
            this.tail = x;
        }

感悟:

由于java没有结构体,所以许多结构定义采用interface进行的,然后通过实现接口,以达到结构体的实现。redis源码给作者带来了极大的震撼,揣摩Sanfilippo的想法然后豁然开朗,比反复刷springboot+mysql+vue这种商城项目框架有趣多了。写完代码后还是忍不住赞叹,卧槽牛逼!打到余麻子!Sanfilippo才是神。(狗头)

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

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

相关文章

【数字量采集1.28】数字信号采集

数字量采集-2024年01月27日-二刷 文章目录 分析考虑一个波形的六要素&#xff1a;项目需求分析&#xff1a;高低电平数字量采集电路设计RS485差分信号传输SP3485 芯片引脚RS485 转 TTL 电路 分析考虑一个波形的六要素&#xff1a; 高电平 低电平 上升时间 下降时间 频率/周期 …

Vue2 VS Vue3 生命周期

一、生命周期的概念 Vue组件实例在创建时要经历一系列的初始化步骤&#xff0c;在此过程中Vue会在合适的时机&#xff0c;调用特定的函数&#xff0c;从而让开发者有机会在特定阶段运行自己的代码&#xff0c;这些特定的函数统称为&#xff1a;生命周期钩子&#xff08;也会叫…

【C++中的STL】常用算法3——常用拷贝和替换算法

常用算法3 copyreplacereplace_ifswap 1、 copy容器内指定范围的元素拷贝到另一个容器中 2、 replace将容器内指定的旧元素修改为新元素 3、 replace_if容器内指定范围满足条件的元素替换为新元素 4、 swap互换两个容器的元素 copy 容器内指定范围的元素拷贝到另一个容器中…

布隆过滤器介绍及实战应用(防止缓存穿透)

布隆过滤器介绍 布隆过滤器&#xff08;Bloom Filter&#xff09;是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多&#xff0c;缺点是有一…

最全全国十七个数据入表和资产化案例深度解析

2024年1月1日起&#xff0c;财政部会计司发布的《企业数据资源相关会计处理暂行规定》正式施行&#xff0c;规定为数据资源的会计处理提供了明确的指导原则。这一里程碑事件也标志着我国在数据资产入表领域正式进入实际操作阶段&#xff0c;随后&#xff0c;数据资产入表在全国…

[BJDCTF 2020]Easy

运行之后是这个东西 我们直接IDA暴力打开 结果main函数啥也不是 &#xff08;看其他人的wp知道了照que函数&#xff09; 我也不知道咋找的&#xff0c;可能真要硬找吧 int ques() {int v0; // edxint result; // eaxint v2[50]; // [esp20h] [ebp-128h] BYREFint v3; // [e…

在vscode里面聊微信

### 1、源起 事情是这样的&#xff0c;某天下午&#xff0c;我在做项目的时候被人事叫去谈话&#xff0c;说些有的没得&#xff0c;但是我注意到她说我不要玩微信&#xff0c;“我好几次都看到你在和别人聊微信”之类的话&#xff0c;所以我打算在ide工具的命令行中聊微信&…

聊聊效能与敏捷的差异

这是鼎叔的第八十四篇原创文章。行业大牛和刚毕业的小白&#xff0c;都可以进来聊聊。 欢迎关注本专栏和微信公众号《敏捷测试转型》&#xff0c;星标收藏&#xff0c;大量原创思考文章陆续推出。 近期&#xff0c;TesterHome社区小道消息播客直播间邀请了鼎叔&#xff0c;与…

程序员的基本素养之——R语言起源、特点以及应用

R语言是一种功能强大的数据分析、统计建模、可视化、 免费、开源且跨平台的编程语言 作为用于数据统计的必备技能语言&#xff0c;博主目前正在对R语言进行基本的学习&#xff0c;这也是生物信息学领域进行统计分析的必备语言之一。下面跟我来一起看看吧&#xff01; R语言是一…

产品解读 | 新一代湖仓集存储,多模型统一架构,高效挖掘数据价值

星环科技TDH一直致力于给用户带来高性能、高可靠的一站式大数据基础平台&#xff0c;满足对海量数据的存储和复杂业务的处理需求。 同时在易用性方面持续深耕&#xff0c;降低用户开发和运维成本&#xff0c;让数据处理平民化&#xff0c;助力用户以更便捷、高效的方式去挖掘数…

【PyTorch】n卡驱动、CUDA Toolkit、cuDNN全解安装教程

文章目录 GPU、NVIDIA Graphics Drivers、CUDA、CUDA Toolkit和cuDNN的关系使用情形判断仅仅使用PyTorch使用torch的第三方子模块 安装NVIDIA Graphics Drivers&#xff08;可跳过&#xff09;前言Linux法一&#xff1a;图形化界面安装&#xff08;推荐&#xff09;法二&#x…

第十三章认识Ajax(四)

认识FormData对象 FormData对象用于创建一个表示HTML表单数据的键值对集合。 它可以用于发送AJAX请求或通过XMLHttpRequest发送表单数据。 以下是FormData对象的一些作用&#xff1a; 收集表单数据&#xff1a;通过将FormData对象与表单元素关联&#xff0c;可以方便地收集表…

【GitHub项目推荐--建一个 ChatGPT 机器人】【转载】

建一个 ChatGPT 机器人 bot-on-anything 它可以将 ChatGPT 等算法模型应用于各类平台。目前&#xff0c;它已经可以接入到个人微信、公众号、QQ、Telegram、Gmail邮箱、Slack 等待&#xff0c;并计划接入Web、企业微信、钉钉等。 通过使用该开源项目&#xff0c;开发者可以通…

一天吃透面试八股文

内容摘自我的学习网站&#xff1a;topjavaer.cn 分享50道Java并发高频面试题。 线程池 线程池&#xff1a;一个管理线程的池子。 为什么平时都是使用线程池创建线程&#xff0c;直接new一个线程不好吗&#xff1f; 嗯&#xff0c;手动创建线程有两个缺点 不受控风险频繁创…

机器学习之numpy库

机器学习之numpy库 numpy库概述numpy库历史numpy的核心numpy基础ndarray数组内存中的ndarray对象ndarray数组对象的特点ndarray数组对象的创建ndarray对象属性的基本操作数组的维度元素的类型数组元素的个数数组元素索引(下标) ndarray对象数组的自定义类型切片操作一维数组切片…

【LTSpice】导入第三方元件库 之 subckt文件类型

LTSpice想要导入第三方的元件库&#xff0c;网上教程非常多。这里记录一下一种不用include命令实现、以后可以直接在component里面添加的 subckt文件的导入。过程比较复杂。 本文只讲解subckt文件&#xff01;如果发现文件里有.SUBCKT这样的文字&#xff0c;说明可以用本文的方…

[UI5 常用控件] 03.Icon, Avatar,Image

文章目录 前言1. Icon2. Avatar2.1 displayShape2.2 initials2.3 backgroundColor2.4 Size2.5 fallbackIcon2.6 badgeIcon2.7 badgeValueState2.8 active 3. Image 前言 本章节记录常用控件Title,Link,Label。 其路径分别是&#xff1a; sap.m.Iconsap.m.Avatarsap.m.Image 1…

贪吃蛇项目(基于C语言和数据结构中的链表)

建立文件 首先先建立3个文件。 Snake.h 函数的声明 Snake.c 函数的定义 Test.c 贪吃蛇的测试 分析项目 我们分析这整个项目 建立节点 首先在我们实现游戏开始的部分之前&#xff0c;我们要先创建贪吃蛇的节点&#xff0c;再由此创建整个贪吃蛇所包含的一些信息&#…

【王道数据结构】【chapter2线性表】【P44t17~t20】【统考真题】

目录 2009年统考 2012年统考 2015年统考 2019年统考 2009年统考 #include <iostream>typedef struct node{int data;node* next; }node,*list;list Init() {list head(list) malloc(sizeof (node));head->next nullptr;head->data-1;return head; }list Buyne…

机器学习 | 如何使用 Seaborn 提升数据分析效率

Seaborn和Matplotlib都是Python可视化库&#xff0c;它们都可以用于创建各种类型的图表。但是&#xff0c;Seaborn 和Matplotlib在概念和设计上有一些不同。 Matplotlib虽然已经是比较优秀的绘图库了&#xff0c;但是它有个今人头疼的问题&#xff0c;那就是API使用过于复杂&am…