当前位置: 首页 > 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+ 树。


相关文章:

  • MATLAB图像加密案例
  • 城市智控 | 废弃物分类可视化管理平台
  • MySQL 索引不生效的情况
  • python 桌面程序开发简述及示例
  • TS 常用类型
  • Redis宣布再次开源
  • 从原理到实战讲解回归算法!!!
  • ESP-ADF esp_dispatcher组件之audio_service子模块状态控制函数详解
  • pytest——参数化
  • 【dify—10】工作流实战——文生图工具
  • 精益数据分析(37/126):深度剖析SaaS模式下的参与度与流失率指标
  • 游戏引擎学习第254天:重新启用性能分析
  • C++析构函数详解
  • Synthesis的分类
  • cat file.tar.gz | tar -xzf - -C /target/dir两个减号之间为什么有个空格?是写错了吗?(管道命令后续)
  • JavaScript基础-赋值运算符
  • Windows 使用set和setx设置环境变量(skywalk3)
  • 区块链+IoT:创新场景落地背后的技术攻坚战
  • 驱动开发系列56 - Linux Graphics QXL显卡驱动代码分析(三)显示模式设置
  • Java 实现socket VAD通讯客户端
  • 3:0战胜日本队,中国羽毛球队挺进2025苏迪曼杯决赛
  • 媒体:机票盲盒值不值得开?年轻人正用行为博弈的逻辑重构规则
  • 女冰队长于柏巍,拒绝被年龄定义
  • 关于“十五五”,在上海召开的这场座谈会释放最新信号
  • 山西太原一小区发生爆炸,造成1人遇难21人受伤2人失联
  • 中国海油总裁:低油价短期影响利润,但也催生资产并购机会