跳表:高效索引的神奇算法

跳表(Skip List)的设计思想基于二叉查找树和链表,它采用了一种多层次的思路,通过随机化的方式在每个节点添加多个后继节点,从而实现快速查找

跳表是一种数据结构,它以一种分层的方式来存储数据,使得查找、插入和删除操作的时间复杂度可以达到O(log n)。跳表的思想可以追溯到1989年,由William Pugh提出。

具体来说,跳表由多个层组成,每个层都是一个有序链表。最低层是一个普通的链表,每个节点包含一个指向下一个节点的指针。而在更高层,每个节点包含多个指针,分别指向不同后继节点。这些指针按一定的概率分布添加到各层,使得查找、插入和删除操作可以在对数时间内完成

基本原理

对于有序的单链表来说,如果要查询链表元素,只能从头到尾依次遍历链表,时间复杂度为O(n),效率不高

跳表通过使用多级索引来加速查找操作。每个索引层都是原链表的一个子集,其中每个节点具有指向下一层和同一层的节点的指针。通过这种方式,跳表可以在平均情况下实现对数时间O(log n)复杂度的查找、插入和删除操作

是经典的以空间换时间的思想,通过索引层的存在,让跳表具有类似于二分查找的性质,从而提高了操作效率

特点

跳表的设计思想具有以下特点:

  1. 分层结构:跳表采用多层次的思路,每个层次都是一个有序链表。通过增加层次,可以使得查找、插入和删除操作的时间复杂度降低。
  2. 随机化:在跳表的每一层,节点的指针不是固定的,而是随机化的。这种随机化可以保证在查找、插入和删除操作中,数据在各层之间的分布相对均匀,从而提高查找效率。
  3. 动态调整:跳表的层次不是固定的,可以根据需要进行动态调整。当插入或删除操作导致某一层的元素数量过少时,可以将其与下一层合并;反之,当某一层的元素数量过多时,可以将其拆分成两个层。这种动态调整可以保持跳表的性能稳定。
  4. 空间换时间:跳表需要额外的空间来存储各层的节点和指针。但是,由于跳表的查找、插入和删除操作的时间复杂度较低,因此可以认为是以空间换取了时间

实现细节

跳表的实现涉及到一些细节,包括索引层的构建和维护、插入和删除操作的处理、以及查找操作的实现等

具体操作的图文推荐参考Redis进阶 - 数据结构:底层数据结构详解

随机性

跳表的索引层是通过随机化构建的,通过一定的概率来决定节点是否添加到上一级索引中。这种随机性的引入有助于跳表在平均情况下提供对数时间复杂度的查找操作

当原始链表中元素数量足够大,且抽取足够随机的话,得到的索引是均匀的(固定间隔时容易造成索引分布不均匀)

private int randomLevel() {
    int lv = 1;
    /* 随机生成 lv */
    while (random.nextDouble() < P_FACTOR && lv < MAX_LEVEL) {
        lv++;
    }
    return lv;
}

Redis 的 zset 中 P_FACTOR 设定的 0.25

插入

  1. 从跳表的顶层索引开始,逐层向下遍历,记录每层要插入的位置(最右边小于待插入元素的节点)
  2. 根据一定的概率,决定是否在较低层次上插入新节点
  3. 对于需要插入的层,逐层赓续索引,插入新节点
public void add(int num) {
    // 记录一下插入位置每层前一位置的节点, 需要将他们的指向全部更新为插入节点
    SkiplistNode[] update = new SkiplistNode[MAX_LEVEL];
    Arrays.fill(update, head);
    SkiplistNode curr = this.head;
    for (int i = level - 1; i >= 0; i--) {
        /* 找到第 i 层小于且最接近 num 的元素*/
        while (curr.forward[i] != null && curr.forward[i].val < num) {
            curr = curr.forward[i];
        }
        update[i] = curr;
    }
    int lv = randomLevel();
    level = Math.max(level, lv);
    SkiplistNode newNode = new SkiplistNode(num, lv);
    for (int i = 0; i < lv; i++) {
        /* 对第 i 层的状态进行更新,将当前元素的 forward 指向新的节点 */
        newNode.forward[i] = update[i].forward[i];
        update[i].forward[i] = newNode;
    }
}

删除

  1. 从跳表的顶层索引开始,逐层向下遍历,记录每层要删除的位置(最右边小于待插入元素的节点)
  2. 从最底层开始,逐层向上遍历,更新索引层的指针,将指向被删除元素的节点指针更新为右侧节点
  3. 从顶层索引依次遍历,若该层已空,更新层数
public boolean erase(int num) {
    SkiplistNode[] update = new SkiplistNode[MAX_LEVEL];
    SkiplistNode curr = this.head;
    for (int i = level - 1; i >= 0; i--) {
        /* 找到第 i 层小于且最接近 num 的元素*/
        while (curr.forward[i] != null && curr.forward[i].val < num) {
            curr = curr.forward[i];
        }
        update[i] = curr;
    }
    curr = curr.forward[0];
    /* 如果值不存在则返回 false */
    if (curr == null || curr.val != num) {
        return false;
    }
    for (int i = 0; i < level; i++) {
        if (update[i].forward[i] != curr) {
            break;
        }
        /* 对第 i 层的状态进行更新,将 forward 指向被删除节点的下一跳 */
        update[i].forward[i] = curr.forward[i];
    }
    /* 更新当前的 level */
    while (level > 1 && head.forward[level - 1] == null) {
        level--;
    }
    return true;
}

查找

跳表的查询操作利用了索引层的结构,通过逐层向下遍历来快速定位目标元素。每一层的遍历都是基于上一层的结果,根据比较结果决定向右还是向下移动。这样的设计可以提高查询效率

  1. 从跳表的顶层索引开始,逐层向下遍历,直到找到目标元素或者达到原链表的最底层
    • 目标值大于下一个节点时,搜索当前层的下一个节点(向右移动)
    • 目标值小于下一个节点时,搜索下一层(向下移动)
  2. 比较搜索到的最底层节点值,相等则返回;否则表示目标元素不存在于跳表中
public boolean search(int target) {
    SkiplistNode curr = this.head;
    for (int i = level - 1; i >= 0; i--) {
        /* 找到第 i 层小于且最接近 target 的元素*/
        while (curr.forward[i] != null && curr.forward[i].val < target) {
            curr = curr.forward[i];
        }
    }
    curr = curr.forward[0];
    /* 检测当前元素的值是否等于 target */
    if (curr != null && curr.val == target) {
        return true;
    }
    return false;
}

Java 实现源码

class Skiplist {
    static final int MAX_LEVEL = 32;
    static final double P_FACTOR = 0.25;
    private SkiplistNode head;
    private int level;
    private Random random;

    public Skiplist() {
        this.head = new SkiplistNode(-1, MAX_LEVEL);
        this.level = 0;
        this.random = new Random();
    }

    public boolean search(int target) {
        SkiplistNode curr = this.head;
        for (int i = level - 1; i >= 0; i--) {
            /* 找到第 i 层小于且最接近 target 的元素*/
            while (curr.forward[i] != null && curr.forward[i].val < target) {
                curr = curr.forward[i];
            }
        }
        curr = curr.forward[0];
        /* 检测当前元素的值是否等于 target */
        if (curr != null && curr.val == target) {
            return true;
        }
        return false;
    }

    public void add(int num) {
        // 记录一下插入位置每层前一位置的节点, 需要将他们的指向全部更新为插入节点
        SkiplistNode[] update = new SkiplistNode[MAX_LEVEL];
        Arrays.fill(update, head);
        SkiplistNode curr = this.head;
        for (int i = level - 1; i >= 0; i--) {
            /* 找到第 i 层小于且最接近 num 的元素*/
            while (curr.forward[i] != null && curr.forward[i].val < num) {
                curr = curr.forward[i];
            }
            update[i] = curr;
        }
        int lv = randomLevel();
        level = Math.max(level, lv);
        SkiplistNode newNode = new SkiplistNode(num, lv);
        for (int i = 0; i < lv; i++) {
            /* 对第 i 层的状态进行更新,将当前元素的 forward 指向新的节点 */
            newNode.forward[i] = update[i].forward[i];
            update[i].forward[i] = newNode;
        }
    }

    public boolean erase(int num) {
        SkiplistNode[] update = new SkiplistNode[MAX_LEVEL];
        SkiplistNode curr = this.head;
        for (int i = level - 1; i >= 0; i--) {
            /* 找到第 i 层小于且最接近 num 的元素*/
            while (curr.forward[i] != null && curr.forward[i].val < num) {
                curr = curr.forward[i];
            }
            update[i] = curr;
        }
        curr = curr.forward[0];
        /* 如果值不存在则返回 false */
        if (curr == null || curr.val != num) {
            return false;
        }
        for (int i = 0; i < level; i++) {
            if (update[i].forward[i] != curr) {
                break;
            }
            /* 对第 i 层的状态进行更新,将 forward 指向被删除节点的下一跳 */
            update[i].forward[i] = curr.forward[i];
        }
        /* 更新当前的 level */
        while (level > 1 && head.forward[level - 1] == null) {
            level--;
        }
        return true;
    }

    private int randomLevel() {
        int lv = 1;
        /* 随机生成 lv */
        while (random.nextDouble() < P_FACTOR && lv < MAX_LEVEL) {
            lv++;
        }
        return lv;
    }
}

class SkiplistNode {
    int val;
    SkiplistNode[] forward;

    public SkiplistNode(int val, int maxLevel) {
        this.val = val;
        this.forward = new SkiplistNode[maxLevel];
    }
}

参考资料:

  1. Skip List–跳表
  2. Redis进阶 - 数据结构:底层数据结构详解

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

百面算法工程师 | 分类和聚类

目录 6.1 为什么正确率有时不能有效评估分类算法&#xff1f; 6.2 什么样的分类器最好&#xff1f; 6.3 什么是聚类&#xff0c;你知道哪些聚类算法&#xff1f; 6.4 K-Means聚类算法如何调优? 6.5 K-Means聚类算法如何选择初始点? 6.6 K-Means聚类聚的是特征还是样本 …

一文掌握:数据湖是什么?可不是数据仓库

一、什么是数据湖 数据湖&#xff08;Data Lake&#xff09;是指一个大型数据存储和处理系统&#xff0c;它能够存储各种类型和格式的数据&#xff0c;包括结构化数据、半结构化数据和非结构化数据。数据湖的目的是为了让企业可以更好地管理和利用大量的数据&#xff0c;以便进…

Email API的安全性如何保障?API发信技巧?

Email API有哪些主要功能&#xff1f;如何选择邮箱API进行集成&#xff1f; Email API在企业和个人用户之间发挥着举足轻重的作用。然而&#xff0c;随着Email API的广泛应用&#xff0c;其安全性问题也逐渐凸显出来。那么&#xff0c;Email API的安全性究竟如何保障呢&#x…

基于 Grassmannian Manifold的动态图嵌入学习的脑网络时空枢纽识别

Spatiotemporal Hub Identification in Brain Network by Learning Dynamic Graph Embedding on Grassmannian Manifold 摘要 神经成像技术的进步使得测量不同大脑区域之间的连接随时间演变成为可能。新出现的证据表明&#xff0c;一些关键的大脑区域&#xff0c;称为枢纽节点…

adb工具使用

作者简介&#xff1a; 一个平凡而乐于分享的小比特&#xff0c;中南民族大学通信工程专业研究生在读&#xff0c;研究方向无线联邦学习 擅长领域&#xff1a;驱动开发&#xff0c;嵌入式软件开发&#xff0c;BSP开发 作者主页&#xff1a;一个平凡而乐于分享的小比特的个人主页…

社会工程渗透测试教程(二)

原文&#xff1a;annas-archive.org/md5/db987a87e1478b8a8617c263c631b477 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第六章&#xff1a;通过有效的威胁建模确保价值 Richard Ackroyd&#xff0c;随机风暴有限公司高级安全工程师 大多数客户意识到他们需要社会…

20.Unity飞机大战游戏

1任务&#xff1a;使背景图动起来 2任务&#xff1a;飞机换帧动画 3任务&#xff1a;让飞机发射子弹 4任务&#xff1a;敌机出现 5任务&#xff1a;控制飞机 6任务&#xff1a;游戏碰撞逻辑 7任务&#xff1a;另外两种类型的敌机 8任务&#xff1a;拾取奖励物品换枪 9…

RK3568 学习笔记 : u-boot 通过 tftp 网络更新 u-boot自身

前言 开发板型号&#xff1a; 【正点原子】 的 RK3568 开发板 AtomPi-CA1 使用 虚拟机 ubuntu 20.04 收到单独 编译 RK3568 u-boot 使用 rockchip Linux 内核的设备树 【替换】 u-boot 下的 rk3568 开发板设备树文件&#xff0c;解决 u-boot 下千兆网卡设备能识别但是无法 Pi…

vulfocus靶场名称: apache-cve_2021_41773/apache-cve_2021_42013

Apache HTTP Server 2.4.49、2.4.50版本对路径规范化所做的更改中存在一个路径穿越漏洞&#xff0c;攻击者可利用该漏洞读取到Web目录外的其他文件&#xff0c;如系统配置文件、网站源码等&#xff0c;甚至在特定情况下&#xff0c;攻击者可构造恶意请求执行命令&#xff0c;控…

JAVA学习笔记30(线程)

1.线程 1.线程的概念 1.线程是由进程创建的&#xff0c;是进程的一个实体 2.一个进程可以拥有多个线程 2.并发 ​ *同一时刻&#xff0c;多个任务交替执行&#xff0c;造成一种"貌似同时"的错觉&#xff0c;单核cpu实现的多任务就是并发 3.并行 ​ *同一时刻&…

电商平台业务及架构演变史

不少人认为电商系统很简单&#xff0c;因为现在做电商的太多了&#xff0c;看到的电商产品也多。看来看去产品都差不多&#xff0c;没什么特别。 其实中国电商发展已有20多年历史&#xff0c;电商以销售为核心连接着研、产、供、销、服整套的信息系统体系。其中的设计并没有那…

Mongodb支持事务吗?

一、概念 1.1、MongoDB事务简介 MongoDB 是一个非关系型数据库管理系统&#xff0c;最初并不支持事务。然而&#xff0c;随着时间的推移&#xff0c;MongoDB 在其4.0版本中引入了多文档事务支持&#xff0c;使得在单个集合中执行多个操作成为可能。 In MongoDB, an operation…

【MySQL探索之旅】多表查询

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 |《MySQL探索之旅》 |《Web世界探险家》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更…

CCF PTA 2023年5月C++富有的大壮

【问题描述】 给在一个神秘的国度&#xff0c;有一种多拿多得的疯狂游戏&#xff0c;某日大壮去参赛&#xff0c;在规定区域内里面有 N(N≤100) 堆金币&#xff0c;第i堆金币的总重量和总价值分别是mi,vi(1≤ mi,vi≤100)。大壮有一个承重量为T(T≤1000) 的背包&#xff0c;但…

Mac下XDebug安装

文章目录 1、下载对应的版本2、编译XDebug3、配置XDebug4、配置PhpStormDebug一下 前置工作 Mac下安装HomebrewMac下brew安装php7.4 1、下载对应的版本 首先按照支持的版本和兼容性来下载对应的版本&#xff0c;此表列出了仍支持哪些 Xdebug 版本&#xff0c;以及哪些版本可用…

vue框架中的组件通信

vue框架中的组件通信 一.组件通信关系二.父子通信1.props 校验2.prop & data、单向数据流 二.非父子通信-event bus 事件总线三.非父子通信 (拓展) - provide & inject四.v-model简化父子通信代码五. .sync修饰符 一.组件通信关系 组件关系分类&#xff1a; 1.父子关系…

2024接口自动化测试高频面试题【建议收藏】

一、json和字典的区别&#xff1f; json就是一个文本、字符串&#xff1b;有固定的格式&#xff0c;格式长的像python字典和列表的组合&#xff1b;以key-value的键值对形式来保存数据&#xff0c;结构清晰&#xff0c;。可以说是目前互联网项目开发中最常用的一种数据交互格式…

文件I/O基础-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板

本章将介绍Linux应用编程中最基础的知识&#xff0c;即文件I/O&#xff08;Input/Output&#xff09;。文件I/O指的是对文件进行读写操作&#xff0c;在Linux系统中一切皆文件&#xff0c;这是Linux系统设计的核心理念&#xff0c;因此文件I/O操作既是基础又是最重要的部分。本…

【webrtc】m114自己实现的PrioritizedPacketQueue及优先级处理

G:\CDN\WEBRTC-DEV\libwebrtc_build\src\modules\pacing\prioritized_packet_queue.h跟m98不同 :webrtc】m98 RoundRobinPacketQueue的优先级处理,m114直接使用taskqueue顺序处理了。甚至自己实现了优先级队列感觉简化了实现,更为清晰 易读,但是去掉了码率低就优先的逻辑。1…

浮杯式轴向柱塞泵(浮杯泵)应用前景较好 但目前产业化规模小

浮杯式轴向柱塞泵&#xff08;浮杯泵&#xff09;应用前景较好 但目前产业化规模小 浮杯式轴向柱塞泵简称浮杯泵&#xff0c;是利用缸体与柱塞间的相对运动改变腔体容积完成吸排油的一类柱塞泵。浮杯泵是基于浮杯原理开发出来的&#xff0c;浮杯原理是继斜盘式和斜轴式之后一种…