深入理解跳表:多层索引加速查找的经典实现

跳表(Skip List)是一种多层有序链表结构,通过引入多级索引加速查找,其核心设计类似于“立体高速公路系统”,底层是原始链表,上面有各种高度的"高架桥"。

高层道路跨度大,连接远方节点;底层道路密集,保留所有细节。

跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

它在性能上和红黑树AVL树不相上下,但是跳表的原理非常简单,实现也比红黑树简单很多。

跳表就能让链表拥有近乎的接近二分查找的效率的一种数据结构,其原理依然是给上面加若干层索引,优化查找速度,典型的空间换取时间的方式
在这里插入图片描述

底层(Level 0)完整的有序单链表,包含所有元素,例如 1 → 3 → 5 → 7 → 9。

上层索引(Level 1~N)

  • 每层节点数约为下层的 1/2(概率性生成,如概率P=0.5时,一般情况下为:0.25(redis使用的就是0.25),表示的是上一层的节点数=当前层*0.25)。
  • 高层节点稀疏但跨度大,底层节点密集。

示例结构(3层跳表):

Level 2: 1 ---------------------> 9  
Level 1: 1 -----> 5 -----> 7 -----> 9  
Level 0: 13579  

每个跳表节点包含一个值一个指针数组forward

这个数组长度就是节点的层级数,指向同层下一个节点。

在Redis的实现中,节点还包含后退指针(backward)和跨度(span)信息

1.查询操作

非常的简单,思想就是二分思想
在这里插入图片描述

// search
public SkipNode<T> search(int key) {// 初始化开始节点为头节点SkipNode<T> p = head;// 在当前的节点不是空的情况下,有三种情况:while (p != null) {if (p.getKey() == key) {// 如果当前节点的key等于要查找的key,则返回当前节点return p;} else if (p.getNext() == null || p.getNext().getKey() >  key) {// 如果右侧没有了,或者右侧的key大于要查找的key,则继续往下找p = p.getDown();} else {p = p.getNext(); // 否则向右找}}return null;
}

2.删除操作

删除操作比起查询稍微复杂一些,但是比插入简单。

删除需要改变链表结构所以需要处理好节点之间的联系。对于删除操作你需要谨记以下几点:

  1. 删除当前节点和这个节点的前后节点都有关系
  2. 删除当前层节点之后,下一层该key的节点也要删除,一直删除到最底层
    在这里插入图片描述

例如上图删除10节点,流程如下:

  • team=head 从 team 出发,7 < 10 向右
  • 右侧为null只能向下;
  • 右侧为10在当前层删除10节点然后向下继续查找下一层10节点;
  • 8 < 10 向右;第五步右侧为10删除该节点并且team向下。
  • team为null说明删除完毕退出循环。
// delete
public void delete(int key) {SkipNode<T> term = head;  // 初始化当前节点while (term != null) {if (term.getNext() == null || term.getNext().getKey() > key) {// 如果当前节点的右侧没有节点,或者当前节点的右侧的key大于要删除的key// 则继续往下找term = term.getDown();} else {// 否则,当前节点的key等于要删除的keyif (term.getNext().getKey() == key) {// 如果当前节点的key等于要删除的key,则将当前节点的next指向当前节点的next的nextterm.setNext(term.getNext().getNext());term = term.getDown();} else {// 否则,当前节点的key不等于要删除的key,则继续向右找term = term.getNext();}}}
}

3 插入操作

插入需要考虑是否插入索引,插入几层等问题。

由于需要插入删除所以我们肯定无法维护一个完全理想的索引结构,因为它耗费的代价太高。但我们使用随机化的方法去判断是否向上层插入索引。

即产生一个[0-1]的随机数如果小于0.5就向上插入索引,插入完毕后再次使用随机数判断是否向上插入索引。运气好这个值可能是多层索引,运气不好只插入最底层(这是100%插入的)。

但是索引也不能不限制高度,我们一般会设置索引最高值如果大于这个值就不往上继续添加索引了。

具体插入步骤:

步骤1:随机决定层数java

// 为新节点随机决定层数
int insertLevel = randomLevel(); // 比如随机得到3层
private int randomLevel() {int level = 1;// 每次有1/2的概率继续向上while (Math.random() < 0.5 && level < MAX_LEVEL) {//MAX_LEVEL:系统设定的最高层,不是当前的最高层level++;}return level;
}

步骤2:从最高层开始查找

// 从跳表的最高层开始查找,记录每层的插入位置
Node[] update = new Node[MAX_LEVEL];
Node current = header;// 从最高层向下查找
for (int i = MAX_LEVEL - 1; i >= 0; i--) {while (current.next[i] != null && current.next[i].value < targetValue) {current = current.next[i];}update[i] = current; // 记录第i层的插入位置
}

步骤3:逐层插入java

// 从底层开始逐层插入
for (int i = 0; i < insertLevel; i++) {newNode.next[i] = update[i].next[i];update[i].next[i] = newNode;
}

具体示例演示:

  • 随机层数在当前层数内
初始跳表:
Level 3: header ---------------------> 9
Level 2: header -----> 5 -----------> 9  
Level 1: header -----> 5 -----> 7 --> 9
Level 0: header -> 3 -> 5 -> 7 -> 9插入值6,随机层数为2:步骤1:随机决定层数 = 2步骤2:从最高层开始查找
- Level 3: header -> 9 (6 < 9),记录update[3] = header
- Level 2: header -> 5 -> 9 (6 > 5),记录update[2] = 5
- Level 1: header -> 5 -> 7 (6 < 7),记录update[1] = 5
- Level 0: header -> 5 -> 7 (6 < 7),记录update[0] = 5步骤3:逐层插入(只在Level 1Level 0插入)
- Level 1: 5 -> 6 -> 7
- Level 0: 5 -> 6 -> 7结果:
Level 3: header ---------------------> 9
Level 2: header -----> 5 -----------> 9  
Level 1: header -----> 5 -> 6 -----> 7 --> 9
Level 0: header -> 3 -> 5 -> 6 -> 7 -> 9
  • 随机层数超过最高层
初始跳表:
Level 3: header ---------------------> 9
Level 2: header -----> 5 -----------> 9  
Level 1: header -----> 5 -----> 7 --> 9
Level 0: header -> 3 -> 5 -> 7 -> 9插入值6,随机层数为5,当前最高层:4步骤1:随机决定层数 = 5步骤2:从最高层开始查找
- Level 4: header -> null (6 < null),记录update[4] = header
- Level 3: header -> 9 (6 < 9),记录update[3] = header
- Level 2: header -> 5 -> 9 (6 > 5),记录update[2] = 5
- Level 1: header -> 5 -> 7 (6 < 7),记录update[1] = 5
- Level 0: header -> 5 -> 7 (6 < 7),记录update[0] = 5步骤3:创建新节点
Node newNode = new Node(6, 5); // 值6,层数5步骤4:从底层开始逐层插入
//由于随机插入的层数是新的最高层
//所以逐层插入(在Level 4、Level 3、Level 2、Level 1、Level 0插入))- Level 4: header -> 6 -> null
- Level 3: header -> 6 -> 9
- Level 2: header -> 5 -> 6 -> 9
- Level 1: header -> 5 -> 6 -> 7 -> 9
- Level 0: header -> 3 -> 5 -> 6 -> 7 -> 9

为何这样设计?

  1. 查找需要从高层开始,因为高层跨度大
  2. 插入需要从底层开始,因为需要维护指针关系

为何要限制最高层数:

  1. 内存使用:防止层数过多导致内存浪费
  2. 性能考虑:层数过多反而影响性能
  3. 实用性:实际应用中很少需要超过32层
  4. 概率分析:32层已经足够处理大量数据

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

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

相关文章

Flutter 视频播放器——flick_video_player 介绍与使用

在移动端应用中&#xff0c;视频播放是一个常见的功能场景&#xff0c;例如短视频、直播、课程、广告展示等。 Flutter 本身并没有直接提供视频播放器组件&#xff0c;而是依赖第三方库来实现。 今天要介绍的库是 flick_video_player&#xff0c;它基于 video_player 封装&…

编写cmakelists文件常用语句

cmake_minimum_required (VERSION 3.10) 指定最小版本project(XXXX) 指定项目名字 ---------------set(MAIN_EXEC_NAME dwarf_parser) 定义变量${ MAIN_EXEC_NAME } 变量取值set(CMAKE_CXX_STANDARD 14) 指定c14标准&#xff0c;还有11、17、20等标准…

麒麟桌面系统找不到mbr启动,并重新安装grub

根据你提供的情况,“麒麟桌面系统找不到MBR启动”,这通常是由于GRUB引导损坏、MBR记录丢失或分区表异常导致的。你可以按照以下步骤重新安装GRUB并修复MBR启动: ✅ 步骤一:准备工具 使用银河麒麟LiveCD或U盘启动盘(可用Ventoy制作); 启动电脑,选择从U盘或光盘进入Live环…

【音频字幕】构建一个离线视频字幕生成系统:使用 WhisperX 和 Faster-Whisper 的 Python 实现

一、背景介绍 对于一端没有字幕外国视频、字幕&#xff0c;在不懂外语的情况下&#xff0c;怎么获取相关内容&#xff1f;作为技术宅&#xff0c;怎么自建搭建一个语音转文字的环境当前AI技术这么发达&#xff1f; 试试 二、系统设计 音频提取(仅仅是视频需要该逻辑、本身就是音…

Linux ALSA架构:PCM_OPEN流程 (二)

一 应用端源码路径: external\tinyalsa\pcm.c external\tinyalsa\pcm_hw.cstruct pcm *pcm_open(unsigned int card, unsigned int device,unsigned int flags, struct pcm_config *config) {...pcm->ops &hw_ops;pcm->fd pcm->ops->open(card, device,…

tp5的tbmember表闭包查询 openid=‘abc‘ 并且(wx_unionid=null或者wx_unionid=‘‘)

闭包查询 tbmember表闭包查询查询 openid‘abc并且islose0并且islogout0并且&#xff08;wx_unionidnull或者wx_unionid’&#xff09; Db::table(tbmember)->where([openid>abc,islose>0,islogout>0])->where(function ($query){$query->where(wx_unioni…

邪修实战系列(3)

1、第一阶段邪修实战总览&#xff08;9.1-9.30&#xff09; 把第一阶段&#xff08;基础夯实期&#xff09;的学习计划拆解成极具操作性的每日行动方案。这个计划充分利用我“在职学习”的特殊优势&#xff0c;强调“用输出倒逼输入”&#xff0c;确保每一分钟的学习都直接服务…

【GD32】ROM Bootloader、自定义Bootloader区别

Bootloader是应用程序跑起来之前&#xff0c;用于初始化的一段程序&#xff0c;它分为两种&#xff0c;ROM Bootloader、自定义Bootloader。GD32芯片出厂时预烧录在ROM中的Bootloader&#xff08;以下简称ROM Bootloader&#xff09;和自己编写的Bootloader&#xff08;以下简称…

Linux防火墙-Firewalld

一、 概述 按表现形式划分&#xff1a; 软件防火墙&#xff1a; 集成在系统内部&#xff0c;Linux系统&#xff1a; iptables、firewalld、ufw&#xff1b; windows系统下&#xff1a; windows defender 硬件防火墙&#xff1a; 华为防火墙、思科防火墙、奇安信防火墙、深信服防…

【Qt】PyQt、原生QT、PySide6三者的多方面比较

目录 引言 一、基本定义 二、核心对比维度 1. 编程语言与开发效率 2. 功能与 API 兼容性 3. 性能表现 4. 许可证与商业使用 5. 社区与文档支持 三、迁移与兼容性 四、适用场景推荐 五、总结对比表 总结 引言 PySide6、PyQt&#xff08;通常指 PyQt5/PyQt6&#xf…

JavaWeb站内信系统 - 技术设计文档

1. 系统概述1.1 项目背景本系统旨在为企业或社区平台提供一套完整的站内信解决方案&#xff0c;支持用户之间的消息发送、接收、管理等功能&#xff0c;提升用户间的沟通效率。1.2 设计目标实现用户间消息发送和接收支持一对一和一对多消息发送提供消息状态跟踪&#xff08;已读…

Java基础 9.10

1.System类常见方法和案例exit&#xff1a;退出当前程序arraycopy&#xff1a;复制数组元素&#xff0c;比较适合底层调用&#xff0c;一般使用 Arrays.copyOf 完成复制数组int[] src{1,2,3};int[] dest new int[3]; System.arraycopy(src, 0, dest, 0, 3);currentTimeMilens&…

详解flink性能优化

1. 简介 Apache Flink是一个强大的流处理框架&#xff0c;其性能很大程度上取决于内存的使用效率。在大规模数据处理场景中&#xff0c;合理的内存配置和优化可以显著提升Flink作业的性能和稳定性。本文将深入探讨Flink内存优化的各个方面&#xff0c;包括状态后端选择、内存配…

VueFlow的箭头怎么调整

正好最近用到了VueFlow组件&#xff0c;发现箭头默认样式太小&#xff0c;无法体现流程展示&#xff0c;因此翻阅相关资料得出下列方法&#xff0c;有什么更好的方法&#xff0c;大家可以推荐推荐&#xff0c;谢谢。方法1&#xff1a;通过边&#xff08;Edge&#xff09;的样式…

【Python】S1 基础篇 P9 文件处理与异常处理技术

目录文件读取操作读取文件的全部内容相对路径和绝对路径逐行访问文件内容文件写入操作写入单行内容写入多行内容结构化数据的存储异常处理机制理解异常的工作原理ZeroDivisionError异常示例try-except语句块的使用else语句块的正确使用静默失败的合理应用本文将深入探讨Python中…

分布式事务实战手册:从四场业务灾难看方案选型与落地陷阱

在分布式系统的稳定性战役中&#xff0c;数据一致性问题如同潜伏的暗礁。某生鲜电商因分布式事务设计缺陷&#xff0c;在春节促销期间出现"下单成功但无库存发货"的悖论&#xff0c;3小时内产生2300笔无效订单&#xff0c;客服投诉量激增300%&#xff1b;某银行转账系…

Java算法题中的输入输出流

在Java算法题中&#xff0c;处理输入输出主要依赖系统流&#xff08;System.in和System.out&#xff09;&#xff0c;常用的方法总结如下&#xff1a; 一、输入方法&#xff08;读取系统输入&#xff09; 主要通过java.util.Scanner类或BufferedReader类实现&#xff0c;适用于…

墨水屏程序

EPD Reader 基于ESP32-C3的电子墨水屏阅读器&#xff0c;支持ap 配网、sntp 时间同步、txt阅读、天气预报、显示节假日信息、农历显示、自动休眠、web配置等功能。这是在另一个项目 一个rust embassy esp32c3 的练习项目-CSDN博客的基础上修改的 。 界面比较粗糙&#xff0c;以…

Git 创建 SSH 密钥

1.生成 SSH 密钥 打开 Git Bash ssh-keygen -t ed25519 -C "your_email@example.com" 把 ”your_email@example.com“ 改成再 github 注册的邮箱 系统会提示您三次输入: 第一个提示:Enter file in which to save the key (/c/Users/86189/.ssh/id_ed25519): 直接…

当前 AI 的主流应用场景

当前AI技术已深度渗透至社会各领域,2025年的主流应用场景呈现出行业垂直化、交互自然化、决策自主化三大特征。以下从六大核心领域展开分析,结合最新技术突破与规模化落地案例,揭示AI如何重塑人类生产生活范式: 一、智能办公与生产力革命 AI正从工具升级为「数字同事」,…