当前位置: 首页 > news >正文

第 7 篇:跳表 (Skip List):简单务实的概率性选手

前面几篇我们都在探讨各种基于“树”结构的有序表实现,它们通过精巧的平衡策略(高度、颜色、大小)和核心的“旋转”操作来保证 O(log N) 的性能。今天,我们要介绍一位画风完全不同的选手——跳表 (Skip List)。它不依赖树形结构,也不需要复杂的旋转,而是巧妙地利用概率和多层链表,同样实现了平均 O(log N) 的高效查找、插入和删除。

核心思想:链表 + 多级“快车道”索引

跳表的设计灵感非常直观,可以类比我们之前提到的地铁系统:

  1. 基础层 (Level 0): 就是一条普通的有序链表,包含所有的数据元素。这是“站站停”的慢车线,保证了数据的完整性和有序性。
  2. 多级索引层 (Higher Levels): 在基础层之上,随机地抽取一部分节点,形成更高一层的“稀疏”链表,作为索引。层级越高,节点越稀疏,跨度越大,就像地铁的“快车线”、“特快线”。
Level 2:  H --------------------------------> 50 ------------> null|                                   |
Level 1:  H --------> 25 ------------------> 50 --------> 75 -> null|           |                       |           |
Level 0:  H --> 10 --> 25 --> 30 --> 38 --> 50 --> 60 --> 75 --> 90 --> null
(H 代表头节点 Head)
  • 查找过程: 从最高层的“特快线” (Level 2) 开始,向右查找,直到找到最后一个小于目标值的节点(比如要找 65,在 Level 2 找到 50)。然后从该节点下降到下一层“快车线” (Level 1),继续向右查找(从 50 开始,找到 50)。再下降到“慢车线” (Level 0),继续向右查找(从 50 开始,找到 60)。发现下一个节点 75 大于 65,查找结束(如果需要精确查找,则检查 60 的下一个节点是否是 65)。

通过这种方式,高层索引帮助我们快速“跳过”大量节点,大幅减少了查找所需的比较次数。

平衡的“魔法”:随机化层高 (The Magic of Randomness)

跳表是如何决定哪些节点可以进入“快车道”,以及一个节点应该出现在多少层“快车道”上的呢?答案是——随机化 (Randomization)!

当插入一个新节点时:

  1. 它首先肯定会被插入到 Level 0 的基础链表中。
  2. 然后,进行一次“抛硬币”(比如生成一个 0 到 1 的随机数)。如果结果满足某个条件(比如小于概率 p​,通常 p=0.5​ 或 p=0.25​),那么这个节点也被提升到 Level 1,并插入到 Level 1 的链表中。
  3. 如果成功提升到 Level 1,就再次抛硬币,决定是否提升到 Level 2。
  4. 以此类推,直到抛硬币结果不再满足提升条件,或者达到了预设的最大层级 MAX_LEVEL​。

这个随机化的过程,使得:

  • 平均来看,大约有 p​ 比例的 Level i​ 的节点会出现在 Level i+1​。
  • 层级越高,节点越稀疏。
  • 虽然单个节点的层高是随机的,但从整体上看,这种多层结构在概率上是平衡的,能够保证查找、插入、删除操作的平均时间复杂度达到 O(log N)。

权衡与优缺点

  • 优点:

    • 实现相对简单: 相比平衡树复杂的旋转逻辑,跳表的插入和删除主要涉及链表节点的指针修改,逻辑更清晰,代码更容易编写和理解。
    • 插入/删除高效: 操作通常只影响局部节点,平均性能很好,且在并发场景下更容易实现高效的锁策略(相比整棵树可能需要调整的平衡树)。
    • 范围查询友好: 底层是有序链表,执行范围查询非常自然和高效。
  • 缺点:

    • 概率性保证: 它的 O(log N) 性能是平均情况下的,虽然概率极低,但理论上存在性能退化到 O(n) 的最坏情况(比如所有节点都只停留在 Level 0)。而平衡树提供的是确定性的 O(log N) 最坏情况保证。
    • 空间开销: 每个节点平均包含 1 / (1 - p)​ 个 right​ 指针(加上 down​ 指针),相比平衡树的固定 2 个子节点指针,其空间开销通常会稍大一些(但仍然是 O(n) 级别)。

一句话选型总结 (跳表)

跳表: 实现内存有序表时,若看重实现简单性、写性能和范围查询效率,且能接受概率性性能保证,跳表是优秀选择。

实际项目思考 (Java & Beyond)

  • Redis 的 ZSet (Sorted Set): 这是跳表最著名、最成功的应用案例之一!ZSet 需要支持按 Score 排序、快速增删改成员、按 Score 范围查询、按排名查询等多种操作。跳表完美地契合了这些需求,其简单的实现和良好的综合性能是 Redis 选择它的重要原因。
  • LevelDB / RocksDB 的 MemTable: 这些键值存储引擎在内存中使用跳表来组织数据(MemTable),因为跳表支持快速写入和高效的范围扫描,便于后续将数据刷写到磁盘。
  • 高并发有序数据结构: 在一些需要高并发读写的有序场景下,跳表基于链表的操作特性使得其锁粒度更容易控制(例如,可以只锁住相关的节点),相比需要旋转可能影响更大范围节点的平衡树,更容易设计出高性能的并发实现。Java 的 java.util.concurrent.ConcurrentSkipListMap​ 和 ConcurrentSkipListSet​ 就是基于跳表实现的线程安全的有序集合。

跳表以其独特的设计哲学——用简单的概率机制代替复杂的确定性平衡算法——在数据结构的世界里占据了一席之地。它证明了在很多时候,“足够好”的概率性保证加上实现的简单性,比追求理论上的完美更具工程价值。

下一篇,我们将把目光投向处理海量数据、面向磁盘存储场景的王者——B/B+ 树。


http://www.xdnf.cn/news/257329.html

相关文章:

  • 深度理解linux系统—— 进程切换和调度
  • 系统架构设计师:设计模式——结构型设计模式
  • 全国信息素养大赛 图形化挑战赛~复赛练习-在正方形内吗?
  • Python基本语法(自定义函数)
  • 雪碧图的原理,使用
  • 组件通信-$refs、$parent
  • C++--入门基础
  • MQTT 协议与 HTTP 协议的区别
  • 操作符详解:逗号表达式与下标访问和函数调用操作符
  • 论文阅读笔记——TesserAct: Learning 4D Embodied World Models
  • 【unity游戏开发入门到精通——UGUI】UGUI自动布局组件
  • 数值与字典解决方案第二十六讲:FILTER函数在去除数据的方法
  • 【大模型】多模态推理
  • 传奇各职业/战士/法师/道士戒指爆率及出处产出地/圣战/法神/天尊/虹魔/魔血/麻痹/超负载/求婚/隐身/传送/复活/护身/祈祷/火焰
  • 第Y3周:yolov5s.yaml文件解读
  • C++ set和map
  • 【dify—10】工作流实战——文生图工具
  • 深度学习框架PyTorch——从入门到精通(YouTube系列 - 4)——使用PyTorch构建模型
  • 截图软件、画图软件、左右分屏快捷键
  • 读懂 Vue3 路由:从入门到实战
  • 交错轴啮合原理加工齿轮方法有哪些?
  • Java文件上传
  • 历史数据分析——运输服务
  • 泰迪杯特等奖案例学习资料:基于边缘计算与多模态融合的温室传感器故障自诊断系统设计
  • AI Rack架构高速互连的挑战:损耗设计与信号完整性的设计框架
  • 【二叉树】java源码实现
  • 安装了新版本的python解释器,但在命令行窗口使用`--version`无法查看版本信息
  • C++ 项目中的多语言字符串管理方案(支持自动提示与动态加载)
  • 数字智慧方案5874丨智慧交通收费稽核管理体系的构建与思考(44页PPT)(文末有下载方式)
  • Qt C++简单图形界面与绘图实验