数据结构之跳表

跳表(Skip List)是一种基于概率平衡的数据结构,通过多层有序链表实现高效的查找、插入和删除操作。它在最坏情况下时间复杂度为 (O(n)),但通过随机化设计,平均时间复杂度可优化至 (O(\log n)),与平衡二叉搜索树性能相当,但实现更为简单。

1. 什么是跳表?

想象一下,你有一本非常厚的电话簿,里面的名字是按字母顺序排列的。现在你想找 “Zhang San” 这个名字。

  • 方法一(链表/线性查找):从第一页的第一个名字 “A” 开始,一页一页地翻,直到找到 “Z”。这非常慢。
  • 方法二(跳表):聪明的电话簿出版商在书的侧面贴上了一些“索引标签”。比如:
    • 第一个标签指向 “B” 开头的部分。
    • 第二个标签指向 “G” 开头的部分。
    • 第三个标签指向 “M” 开头的部分。
    • 第四个标签指向 “S” 开头的部分。
    • 第五个标签指向 “Z” 开头的部分。

现在你找 “Zhang San”:

  1. 你先看侧面的标签,发现 “Zhang” 的首字母 “Z” 在 “S” 和 “Z” 之间。于是你直接翻到 “S” 标签指向的页面。
  2. 从 “S” 开始,你继续向后翻,因为 “Z” 在 “S” 之后。你可能还会看到一些更细分的标签,比如 “T”, “W”, “X” 等,帮助你更快地定位。
  3. 很快,你就找到了 “Z” 开头的部分,然后在这个小范围内顺序查找,最终找到 “Zhang San”。

跳表就是这种“带有多级索引的有序链表”。它通过在原始有序链表之上建立多级“索引”层,来大幅提升查找效率,使得查找、插入、删除操作都能在接近对数的时间复杂度内完成。


2. 为什么需要跳表?

跳表是为了解决有序链表的痛点而诞生的。

  • 有序链表的优点
    • 插入和删除操作非常快,只需要修改相邻节点的指针,时间复杂度为 O(1)在找到位置后。
  • 有序链表的致命缺点
    • 查找效率极低。由于不能像数组那样通过下标随机访问,查找一个元素必须从头节点开始逐个遍历,时间复杂度为 O(n)。当数据量很大时,这个性能是无法接受的。

我们希望有一种数据结构,既能保持链表高效的插入/删除特性,又能拥有像平衡二叉搜索树那样 O(log n) 级别的查找效率。

跳表就是答案之一。它通过增加空间(建立索引)来换取时间,完美地平衡了查找、插入、删除的性能。


3. 跳表的结构与原理

一个跳表由以下几部分构成:

  1. 基础层:最底层是一个标准的有序链表,包含了所有的元素。
  2. 索引层:在基础层之上,有多层稀疏的“索引”链表。
    • 每一层都是一个有序链表。
    • 第 L+1 层的元素是第 L 层元素的一个子集
    • 上层的每个节点,都有一个指针指向其在下层中相同的节点。
  3. 头节点:所有层的链表都共享一个头节点,方便从最高层开始查找。
  4. 概率性:一个节点出现在多少层索引中,是通过**“抛硬币”**这样的随机算法决定的。这保证了跳表的平衡性,避免了像平衡树那样复杂的旋转操作。

结构示意图:

假设我们有基础链表 1 <-> 3 <-> 4 <-> 5 <-> 7 <-> 8 <-> 10

一个可能的跳表结构如下:

Level 3:  ------------------------> 8 ------------------------> NULL|                        |
Level 2:  ----> 3 ----------------> 8 ------------------------> NULL|     |                  |
Level 1:  ----> 3 ------> 5 ------> 8 ------> 10 -------------> NULL|     |        |        |        |
Level 0:  -> 1 <-> 3 <-> 4 <-> 5 <-> 7 <-> 8 <-> 9 <-> 10 -> NULL

解读:

  • Level 0 是基础层,包含所有元素。
  • Level 1 包含了 {3, 5, 8, 10},它们是 Level 0 的一个子集。
  • Level 2 包含了 {3, 8},是 Level 1 的一个子集。
  • Level 3 只包含了 {8}
  • 每个上层的节点都通过一个向下的指针连接到下层对应的节点。

4. 操作详解

a. 查找

查找是跳表最核心的操作,插入和删除都依赖于它。查找过程遵循“先高层后低层,先大步后小步”的原则。

目标:查找元素 5

  1. 从最高层开始:从头节点 Head 的 Level 3 开始。
  2. Level 3 查找
    • 当前节点是 Head,下一个节点是 8
    • 5 < 8,说明 5 在 Head 和 8 之间。不能在 Level 3 继续向右走了。
    • 向下:移动到 Level 2 的 Head 节点。
  3. Level 2 查找
    • 当前节点是 Head,下一个节点是 3
    • 5 > 3,可以继续向右走。移动到节点 3
    • 在节点 3,它的下一个节点是 8
    • 5 < 8,说明 5 在 3 和 8 之间。不能在 Level 2 继续向右走了。
    • 向下:移动到 Level 1 的节点 3
  4. Level 1 查找
    • 当前节点是 3,下一个节点是 5
    • 5 == 5找到目标!

如果查找 6

  1. … (过程同上,直到 Level 1 的节点 5)
  2. 在 Level 1 的节点 5,下一个节点是 8
  3. 6 < 8,不能向右。
  4. 向下:移动到 Level 0 的节点 5
  5. Level 0 查找
    • 当前节点是 5,下一个节点是 7
    • 6 < 7,且 6 != 5,说明 6 不存在。

查找路径总结Head(L3) -> Head(L2) -> 3(L2) -> 3(L1) -> 5(L1)

b. 插入

插入分为两步:查找位置 和 随机建层

目标:插入元素 6

  1. 查找插入位置

    • 按照查找 6 的方法,找到它在 Level 0 中的前驱节点。从上面的查找过程可知,6 应该插入在 5 和 7 之间。我们需要记录下每一层中 6 的前驱节点,这里主要是 Level 0 的 5
    • 在实际操作中,我们会在查找过程中维护一个 update 数组,记录每一层中待插入位置的前驱节点。
  2. 随机决定层数

    • 这是跳表的精髓。我们通过一个随机函数(比如,模拟抛硬币)来决定新节点 6 要“晋升”到多少层。
    • 算法:初始化层数 level = 1。然后循环,每次有 p (通常 p=1/2 或 1/4) 的概率 level++,直到失败为止。level 不能超过跳表当前的最大层数。
    • 假设我们“抛硬币”的结果是:第一次成功(level=2),第二次成功(level=3),第三次失败。那么新节点 6 的层数就是 3
  3. 插入节点并更新指针

    • 如果新节点的层数 3 大于当前跳表的最大层数(比如是 2),则需要增加一个新的 Level 3 层。
    • 从 level=3 开始,逐层向下插入新节点:
      • Level 3:将 6 插入到 update[3] (即 Head) 之后。Head -> 6
      • Level 2:将 6 插入到 update[2] (即 3) 之后。3 -> 6
      • Level 1:将 6 插入到 update[1] (即 5) 之后。5 -> 6
      • Level 0:将 6 插入到 update[0] (即 5) 之后。5 -> 6 -> 7
    • 同时,要确保新节点 6 在每一层的实例都通过 down 指针连接起来。
c. 删除

删除操作比插入简单,因为它不需要随机建层。

目标:删除元素 5

  1. 查找待删除节点

    • 首先查找 5。在查找过程中,同样需要记录每一层中 5 的前驱节点(update 数组)。
    • 如果找不到 5,则删除失败。
  2. 逐层删除

    • 从 5 存在的最高层开始(比如 Level 1),逐层向下删除。
    • 在每一层,修改前驱节点(update[i])的 next 指针,使其指向 5 的后继节点。
    • Level 1update[1] 是 3,将 3->next 从指向 5 改为指向 8
    • Level 0update[0] 是 4,将 4->next 从指向 5 改为指向 6
    • 这样,5 在所有层中的引用都被移除了,垃圾回收器会自动回收内存。
  3. (可选)清理空层:如果删除某个节点后,最高层只剩下一个头节点,可以考虑移除这一层以节省空间。


5. 性能分析

  • 时间复杂度

    • 查找、插入、删除:平均时间复杂度均为 O(log n)
    • 最坏情况:如果运气极差,所有节点都在同一层,那么跳表就退化成了普通链表,时间复杂度为 O(n)。但由于层数是随机决定的,这种情况的概率极低,可以忽略不计。
  • 空间复杂度

    • 平均空间复杂度为 O(n)
    • 每个节点被包含在第 i 层索引中的概率是 p^(i-1)。一个节点平均包含的指针数是 1/(1-p)
    • 当 p = 1/2 时,每个节点平均有 1 + 1/2 + 1/4 + ... = 2 个指针(一个 next,一个 down)。所以总空间开销大约是基础链表的 2 倍,即 O(2n) = O(n)

6. 跳表的优缺点

优点:

  1. 性能均衡:查找、插入、删除的性能都非常优秀,都是 O(log n)。
  2. 实现简单:相比于红黑树、AVL树等平衡二叉搜索树,跳表的实现逻辑要简单得多,没有复杂的旋转和颜色变换操作。
  3. 天然有序:底层是有序链表,非常适合需要范围查询的场景(如 “查找所有 score 在 100 到 200 之间的元素”)。
  4. 并发友好:由于跳表的修改操作(插入、删除)通常只影响局部节点,不像平衡树那样可能引起全局性的结构调整,因此更容易实现高效的并发控制。Redis 的作者就曾评价,跳表在并发环境下比平衡树更有优势。

缺点:

  1. 空间开销:需要额外的空间来存储多层索引,空间复杂度高于普通链表和平衡树(虽然都是 O(n),但常数因子更大)。
  2. 非最坏情况保证:性能是概率性的,虽然最坏情况概率极低,但不像平衡树那样能提供硬性的性能保证。

7. 实际应用

跳表最著名的应用是在 Redis 中。

  • Redis 的有序集合:Redis 的 ZSET (Sorted Set) 数据结构,在元素数量较少时使用 ziplist(压缩列表)来节省内存,当元素数量超过阈值(zset-max-ziplist-entries)时,会转换为 跳表 + 哈希表 的实现。

    • 跳表:负责按 score 排序,支持高效的范围查找、按 rank 查找等操作。
    • 哈希表:负责建立 member 到 score 的映射,支持 O(1) 复杂度的按 member 查找 score
    • 这种组合完美地发挥了两种数据结构的优势。
  • LevelDB / RocksDB:这些著名的键值存储引擎在其内部内存表(MemTable)的实现中也使用了跳表。


8. 与其他数据结构的对比

特性跳表平衡二叉搜索树 (如红黑树)哈希表
查找O(log n) 平均O(log n) 最坏O(1) 平均
插入O(log n) 平均O(log n) 最坏O(1) 平均
删除O(log n) 平均O(log n) 最坏O(1) 平均
有序性天然有序,范围查询高效天然有序,范围查询高效无序,不支持范围查询
实现复杂度相对简单复杂(旋转/变色)简单
空间开销较高 (约 2n)较低 (n)可能有额外开销(解决冲突)
并发友好性(局部修改)较低(结构调整可能影响大)需要处理哈希冲突和扩容

总结:

跳表是一种非常精巧的数据结构,它用一种简单而优雅的方式,在有序链表的基础上通过“空间换时间”和“随机化”思想,实现了与平衡树相媲美的对数级性能。它实现简单、性能均衡、并发友好,特别适合作为内存数据库索引的底层实现,是程序员工具箱中一个非常有价值的工具。

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

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

相关文章

线程概念,控制

一、线程概念 线程概念&#xff1a;进程内部的一个执行流&#xff0c;轻量化。 观点&#xff1a;进程是系统分配资源的基本单位&#xff0c;线程是CPU调度的基本单位。 在理解线程之前&#xff0c;我们在谈一下虚拟地址空间。 我们都知道进程是通过页表将虚拟地址转化为物理地址…

RabbitMQ 高可用实战篇(Mirrored Queue + Cluster + 持久化整合)

RabbitMQ 高可用实战篇&#xff08;Mirrored Queue Cluster 持久化整合&#xff09;1. 前言 在生产环境中&#xff0c;单节点 RabbitMQ 容易因故障导致消息丢失或业务中断。 通过高可用队列、集群部署和持久化策略&#xff0c;可以保证 消息可靠性、节点容错和持续服务。 本文…

支持向量机:从理论到实践

支持向量机&#xff1a;从理论到实践 文章目录支持向量机&#xff1a;从理论到实践一。理论概述1. 线性可分支持向量机1.1 基本概念与数学形式1.2 函数间隔与几何间隔1.3 间隔最大化与优化问题1.4 拉格朗日对偶理论与求解1.5 支持向量与决策函数2. 近似线性可分数据&#xff08…

LVS与Keepalived详解(二)LVS负载均衡实现实操

文章目录前言一、LVS-DR 模式详解1.1 数据包流向分析1.2 DR 模式的特点二、LVS-DR 集群部署实战2.1 环境准备2.2 配置负载调度器&#xff08;Director Server&#xff09;2.3 配置节点服务器&#xff08;Real Server&#xff09;2.4 测试验证三、前期回顾3.1 LVS 三种工作模式及…

归一化实现原理

归一化&#xff08;Normalization&#xff09;是一种将数据转换到相同尺度的预处理技术&#xff0c;它通常用于让不同特征&#xff08;或数据项&#xff09;具有相同的量纲或范围。在联邦学习中&#xff0c;归一化可以用来处理非独立同分布&#xff08;Non-IID&#xff09;**数…

企业级实战:构建基于Qt、C++与YOLOv8的模块化工业视觉检测系统

一、概述 在追求高效与精密的现代制造业中&#xff0c;自动化光学检测&#xff08;AOI&#xff09;已成为保障产品质量的核心技术。传统的质检流程往往受限于人工效率与主观判断&#xff0c;难以满足大规模、高精度的生产需求。本文旨在研发一套完整的、企业级的工业视觉异常检…

【目标检测】metrice_curve和loss_curve对比图可视化

代码如下&#xff1a; import warnings warnings.filterwarnings(ignore)import os import pandas as pd import numpy as np import matplotlib.pylab as pltpwd os.getcwd()names [model1, model2, model3,ours]plt.figure(figsize(10, 10))plt.subplot(2, 2, 1) for i in …

【LeetCode hot100|Week2】滑动窗口,子串

笔记用于个人复习和巩固&#xff0c;题解非原创&#xff0c;参考LeetCode官方题解以及各个大佬的解法&#xff0c;希望给大家带来帮助&#xff0c;同时笔记也能督促我学习进步 这周主要把滑动窗口和子串的题目刷了一遍 文章目录Week2D1 滑动窗口209. 长度最小的子数组713. 乘积…

vue2纯前端对接海康威视摄像头实现实时视频预览

vue2纯前端对接海康威视摄像头实现实时视频预览一、环境准备二、代码集成1.1 准备webrtcstreamer.js&#xff0c;粘贴即用&#xff0c;不用做任何修改1.2 封装视频组件&#xff0c;在需要视频的地方引入此封装的视频组件即可&#xff0c;也是粘贴即用&#xff0c;注意其中impor…

Android 设置禁止截图和禁止长截图

1.禁止截图 在 Activity 代码中 , 可以在调用 setContentView 函数之前 ,为 Window 窗口对象 设置 LayoutParams.FLAG_SECURE 标志位 , 可以禁止对本界面进行截屏 ,Window 窗口对象 , 可通过 getWindow 方法获取 ,核心代码如下 :getWindow().setFlags(LayoutParams.FLAG_SECUR…

AR 巡检在工业的应用|阿法龙XR云平台

AR 巡检的应用覆盖电力、石油化工、智能制造、轨道交通、冶金等对设备可靠性和安全性要求极高的行业&#xff0c;具体场景包括&#xff1a;电力行业变电站内设备的状态检查&#xff1a;通过 AR 眼镜扫描设备&#xff0c;实时显示设备额定参数、历史故障记录、实时传感器数据&am…

【C++】STL详解(七)—stack和queue的介绍及使用

✨ 坚持用 清晰易懂的图解 代码语言&#xff0c; 让每个知识点都 简单直观 &#xff01; &#x1f680; 个人主页 &#xff1a;不呆头 CSDN &#x1f331; 代码仓库 &#xff1a;不呆头 Gitee &#x1f4cc; 专栏系列 &#xff1a; &#x1f4d6; 《C语言》&#x1f9e9; 《…

深度学习周报(9.8~9.14)

目录 摘要 Abstract 1 LSTM相关网络总结与对比 1.1 理论总结 1.2 代码运行对比 2 量子计算入门 3 总结 摘要 本周首先总结了LSTM、Bi-LSTM与GRU的区别与优缺点&#xff0c;对比了三者实战的代码与效果&#xff0c;还另外拓展了一些循环神经网络变体&#xff08;包括窥视…

Quat 四元数库使用教程:应用场景概述

基础概念 四元数是一个包含四个元素的数组 [x, y, z, w]&#xff0c;其中 x,y,z表示虚部&#xff0c;w 表示实部。单位四元数常用于表示3D空间中的旋转。 1. 创建和初始化函数 create() - 创建单位四元数 应用场景&#xff1a;初始化一个新的四元数对象&#xff0c;通常作为其他…

【Java后端】Spring Boot 多模块项目实战:从零搭建父工程与子模块

如何用 Spring Boot 搭建一个父工程 (Parent Project)&#xff0c;并在其中包含多个子模块 (Module)&#xff0c;适合企业级项目或者需要分模块管理的场景。Spring Boot 多模块项目实战&#xff1a;从零搭建父工程与子模块在日常开发中&#xff0c;我们经常会遇到这样的需求&am…

企业级AI会议系统技术实现:快鹭如何用AI重构会议全流程

摘要 本文深度解析快鹭AI会议系统的核心技术架构&#xff0c;重点探讨其在语音识别、自然语言处理、数据集成和安全防护等方面的技术实现。通过对比传统会议系统的技术痛点&#xff0c;分析快鹭AI如何通过技术创新实现会议筹备时间减少67%、数据调取速度提升100倍的显著效果。…

【CSS学习笔记3】css特性

1css三大特性 1.1层叠性&#xff1a;就近原则&#xff0c;最新定义的样式 1.2继承性&#xff1a;子标签集成父标签的样式&#xff0c;如文本和字号 行高的继承&#xff1a;不加单位指的是当前文字大小的倍数 body {font: 12px/1.5 Microsoft YaHei;color: #be1313;} div {…

[C语言]常见排序算法①

1.排序的概念及常见的排序算法排序在咱们日常生活中十分的常见&#xff0c;就好比是网上购物的时候通常能够选择按照什么排序&#xff0c;比如价格、评论数量、销量等。那么接下来咱们就来了解一些关于排序的概念。排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xf…

文献阅读笔记:RS电子战测试与测量技术文档

信息来源&#xff1a;罗德与施瓦茨&#xff08;Rohde & Schwarz&#xff09;公司关于电子战&#xff08;Electronic Warfare, EW&#xff09;测试与测量解决方案专业技术文档。 该文档由台湾地区应用工程师Mike Wu撰写&#xff0c;核心围绕电子战基础、雷达系统、实战应用及…

别再纠结 Postman 和 Apifox 了!这款开源神器让 API 测试更简单

别再纠结 Postman 和 Apifox 了&#xff01;这款开源神器让 API 测试更简单&#x1f525; 作为一名开发者&#xff0c;你是否还在为选择 API 测试工具而纠结&#xff1f;Postman 太重、Apifox 要联网、付费功能限制多&#xff1f;今天给大家推荐一款完全免费的开源替代方案 ——…