跳表(skipList)

article/2025/8/8 0:07:53

一、为何有skipList这种数据结构的出现

我们知道二分查找算法之所以能达到 O(logn) 这样高效的一个重要原因在于它所依赖的数据结构是数组,数组支持随机访问一个元素,通过下标很容易定位到中间元素。而链表是不支持随机访问的,只能从头到尾依次访问。但是数组有数组的局限性,比如需要连续的内存空间,插入删除操作会引起数组的扩容和元素移动,链表有链表的优势,链表不需要先申请连续的空间,插入删除操作的效率非常高。在很多情况下,数据是通过链表这种数据结构存储的,如果是有序链表,真的就没有办法使用二分查找算法了吗?实际上对有序链表稍加改造,我们就可以对链表进行二分查找。这就是我们要说的跳表。说到底,skipList的出现就是为了实现有序链表的二分查找,使有序链表的查找速度能够媲美它的效率和红黑树以及 AVL 树,而删除和插入的速度又能比他们速度更快。下面我们来看一下,跳表是怎么跳的。这里记住一句话:跳表的查找、插入、删除的时间复杂度都是 O(logn)

二、跳表的特点及结构

总结就是一句话:给有序的链表再加索引,在n级索引的基础上,还可以再加n+1级索引,直至该级索引的索引点只有2个时,就没必要再加索引

先看一下跳表的特点

  1. 由很多层组成
  2. 每一层都是一个有序链表
  3. 最底层的链表包含所有元素
  4. 每个元素插入时随机生成它的leve
  5. 如果一个元素出现在第i层的链表中,则它在i-1层中也会出现
  6. 上层节点可以跳转到下层
  7. 跳表查询、插入、删除的时间复杂度为O(log n),与平衡二叉树接近

跳表的最终结构图:
在这里插入图片描述

三、跳表相关操作

  1. 查找操作,查找的时间复杂度
    在这里插入图片描述
    查找元素的过程是从最高级索引开始,一层一层遍历最后下沉到原始链表。所以,时间复杂度 = 索引的高度 * 每层索引遍历元素的个数。

图中所示,现在到达第 k 级索引,我们发现要查找的元素 x 比 y 大比 z 小,所以,我们需要从 y 处下降到 k-1 级索引继续查找,k-1级索引中比 y 大比 z 小的只有一个 w,所以在 k-1 级索引中,我们遍历的元素最多就是 y、w、z,发现 x 比w大比 z 小之后,再下降到 k-2 级索引。所以,k-2 级索引最多遍历的元素为
w、u、z。其实每级索引都是类似的道理,每级索引中都是两个结点抽出一个结点作为上一级索引的结点。
现在我们得出结论:当每级索引都是两个结点抽出一个结点作为上一级索引的结点时,每一层最多遍历3个结点。
跳表的索引高度 h = log2n,且每层索引最多遍历 3 个元素。所以跳表中查找一个元素的时间复杂度为 O(3*logn),省略常数即:O(logn)。

  1. 空间复杂度

跳表通过建立索引,来提高查找元素的效率,就是典型的“空间换时间”的思想,所以在空间上做了一些牺牲,那空间复杂度到底是多少呢?

假如原始链表包含 n 个元素,则一级索引元素个数为 n/2、二级索引元素个数为 n/4、三级索引元素个数为 n/8 以此类推。所以,索引节点的总和是:n/2 + n/4 + n/8 + … + 8 + 4 + 2 = n-2,空间复杂度是 O(n)。
如下图所示:如果每三个结点抽一个结点做为索引,索引总和数就是 n/3 + n/9 + n/27 + … + 9 + 3 + 1= n/2,减少了一半。所以我们可以通过较少索引数来减少空间复杂度,但是相应的肯定会造成查找效率有一定下降,我们可以根据我们的应用场景来控制这个阈值,看我们更注重时间还是空间。
在这里插入图片描述

  1. 插入数据

插入数据看起来也很简单,跳表的原始链表需要保持有序,所以我们会向查找元素一样,找到元素应该插入的位置。如下图所示,要插入数据6,整个过程类似于查找6,整个的查找路径为 1、1、1、4、4、5。查找到第底层原始链表的元素 5 时,发现 5 小于 6 但是后继节点 7 大于 6,所以应该把 6 插入到 5 之后 7 之前。整个时间复杂度为查找元素的时间复杂度 O(logn)。
在这里插入图片描述
这里有一个需要格外注意的地方:如果只是单纯插入数据而不更新索引,跳表最后也会退化成链表,如下图:
在这里插入图片描述
4. randomLevel() 方法

对于这个问题 ,可能大家普遍想到的方法就是,插入数据后删除原来的索引,再重新建立索引。那么重建索引的重建索引的时间复杂度是多少呢?因为索引的空间复杂度是 O(n),即:索引节点的个数是 O(n) 级别,每次完全重新建一个 O(n) 级别的索引,时间复杂度也是 O(n) 。造成的后果是:为了维护索引,导致每次插入数据的时间复杂度变成了 O(n)。

那有没有其他效率比较高的方式来维护索引呢?假如跳表每一层的晋升概率是 1/2,最理想的索引就是在原始链表中每隔一个元素抽取一个元素做为一级索引。换种说法,我们在原始链表中随机的选 n/2 个元素做为一级索引是不是也能通过索引提高查找的效率呢? 当然可以了,因为一般随机选的元素相对来说都是比较均匀的。如下图所示,随机选择了n/2 个元素做为一级索引,虽然不是每隔一个元素抽取一个,但是对于查找效率来讲,影响不大,比如我们想找元素 16,仍然可以通过一级索引,使得遍历路径较少了将近一半。如果抽取的一级索引的元素恰好是前一半的元素 1、3、4、5、7、8,那么查找效率确实没有提升,但是这样的概率太小了。我们可以认为:当原始链表中元素数量足够大,且抽取足够随机的话,我们得到的索引是均匀的。我们要清楚设计良好的数据结构都是为了应对大数据量的场景,如果原始链表只有 5 个元素,那么依次遍历 5 个元素也没有关系,因为数据量太少了。所以,我们可以维护一个这样的索引:随机选 n/2 个元素做为一级索引、随机选 n/4 个元素做为二级索引、随机选 n/8 个元素做为三级索引,依次类推,一直到最顶层索引。这里每层索引的元素个数已经确定,且每层索引元素选取的足够随机,所以可以通过索引来提升跳表的查找效率。
在这里插入图片描述
那代码该如何实现,才能使跳表满足上述这个样子呢?可以在每次新插入元素的时候,尽量让该元素有 1/2 的几率建立一级索引、1/4 的几率建立二级索引、1/8 的几率建立三级索引,以此类推,就能满足我们上面的条件。现在我们就需要一个概率算法帮我们把控这个 1/2、1/4、1/8 ... ,当每次有数据要插入时,先通过概率算法告诉我们这个元素需要插入到几级索引中,然后开始维护索引并把数据插入到原始链表中。下面开始讲解这个概率算法代码如何实现。
我们可以实现一个 randomLevel() 方法,该方法会随机生成 1~MAX_LEVEL 之间的数(MAX_LEVEL表示索引的最高层数),且该方法有 1/2 的概率返回 1、1/4 的概率返回 2、1/8的概率返回 3,以此类推。

  1. randomLevel() 方法返回 1 表示当前插入的该元素不需要建索引,只需要存储数据到原始链表即可(概率 1/2)
  2. randomLevel() 方法返回 2 表示当前插入的该元素需要建一级索引(概率 1/4)
  3. randomLevel() 方法返回 3表示当前插入的该元素需要建二级索引(概率 1/8) randomLevel() 方法返回 4 表示当前插入的该元素需要建三级索引(概率1/16)
  4. 。。。以此类推

所以,通过 randomLevel() 方法,我们可以控制整个跳表各级索引中元素的个数。重点来了:randomLevel() 方法返回 2 的时候会建立一级索引,我们想要一级索引中元素个数占原始数据的 1/2,但是 randomLevel() 方法返回 2 的概率为 1/4,那是不是有矛盾呢?明明说好的 1/2,结果一级索引元素个数怎么变成了原始链表的 1/4?我们先看下图,应该就明白了。
在这里插入图片描述
假设我们在插入元素 6 的时候,randomLevel() 方法返回 1,则我们不会为 6 建立索引。插入 7 的时候,randomLevel() 方法返回3 ,所以我们需要为元素 7 建立二级索引。这里我们发现了一个特点:当建立二级索引的时候,同时也会建立一级索引;当建立三级索引时,同时也会建立一级、二级索引。所以,一级索引中元素的个数等于 [ 原始链表元素个数 ] * [ randomLevel() 方法返回值 > 1 的概率 ]。因为 randomLevel() 方法返回值 > 1就会建索引,凡是建索引,无论几级索引必然有一级索引,所以一级索引中元素个数占原始数据个数的比率为 randomLevel() 方法返回值 > 1 的概率。那 randomLevel() 方法返回值 > 1 的概率是多少呢?因为 randomLevel() 方法随机生成 1~MAX_LEVEL 的数字,且 randomLevel() 方法返回值 1 的概率为 1/2,则 randomLevel() 方法返回值 > 1 的概率为 1 - 1/2 = 1/2。即通过上述流程实现了一级索引中元素个数占原始数据个数的 1/2。

同理,当 randomLevel() 方法返回值 > 2 时,会建立二级或二级以上索引,都会在二级索引中增加元素,因此二级索引中元素个数占原始数据的比率为 randomLevel() 方法返回值 > 2 的概率。 randomLevel() 方法返回值 > 2 的概率为 1 减去 randomLevel() = 1 或 =2 的概率,即 1 - 1/2 - 1/4 = 1/4。OK,达到了我们设计的目标:二级索引中元素个数占原始数据的 1/4。

以此类推,可以得出,遵守以下两个条件:

  1. randomLevel() 方法,随机生成 1~MAX_LEVEL 之间的数(MAX_LEVEL表示索引的最高层数),且有 1/2的概率返回1、1/4的概率返回 2、1/8的概率返回 3 …
  2. randomLevel() 方法返回 1 不建索引、返回2建一级索引、返回 3建二级索引、返回 4 建三级索引 …

就可以满足我们想要的结果,即:一级索引中元素个数应该占原始数据的 1/2,二级索引中元素个数占原始数据的 1/4,三级索引中元素个数占原始数据的 1/8 ,依次类推,一直到最顶层索引。

randomLevel() 方法代码:

// 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。// 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。// 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 ://        50%的概率返回 1//        25%的概率返回 2//      12.5%的概率返回 3 ...private int randomLevel() {int level = 1;while (Math.random() < SKIPLIST_P && level < MAX_LEVEL) {level += 1;}return level;}

完整的插入流程:
在这里插入图片描述
整个插入过程的路径与查找元素路径类似, 每层索引中插入元素的时间复杂度 O(1),所以整个插入的时间复杂度是 O(logn)。

  1. 删除数据
    在这里插入图片描述
    删除元素的时间复杂度:
    删除元素的过程跟查找元素的过程类似,只不过在查找的路径上如果发现了要删除的元素 x,则执行删除操作。跳表中,每一层索引其实都是一个有序的单链表,单链表删除元素的时间复杂度为 O(1),索引层数为 logn 表示最多需要删除 logn 个元素,所以删除元素的总时间包含 查找元素的时间 加 删除 logn个元素的时间 为 O(logn) + O(logn) = 2 O(logn),忽略常数部分,删除元素的时间复杂度为 O(logn)。

本文转自


http://chatgpt.dhexx.cn/article/2ZnoLYGN.shtml

相关文章

跳表 skiplist 简介

跳表 skiplist 跳表 (Skip List) 是由 William Pugh 在 1990 年发表的文章 Skip Lists: A Probabilistic Alternative toBalanced Trees 中描述的一种查找数据结构&#xff0c;支持对数据的快速查找&#xff0c;插入和删除。 对于 AVL 树、红黑树等平衡树&#xff0c;在插入过…

SkipList(跳跃表)详解

Introduction: skiplist本质上也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff08;Searching&#xff09;&#xff0c;即根据给定的key&#xff0c;快速查到它所在的位置&#xff08;或者对应的value&#xff09; 一般用于解决查找问题的数据结构分为两个大类…

docker中国镜像

一,如果是像redis,mysql等官方的镜像,直接配置阿里云的镜像就可以 1,注册阿里云账号,并登录 2,打开这个网址 https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors 3,页面上会教怎么设置,如下面的截图 二,如果是私有仓库的镜像 非常感谢原作者:https://www.jiansh…

Docker 使用国内镜像仓库

Docker 使用国内镜像仓库 1、问题描述2、总结 1、问题描述 由于某些原因&#xff0c;导致Docker镜像在国内下载速度特别慢。所以为了沉浸式开发。最好切换为国内源。这里以163 的镜像仓库举例。首先修改/etc/docker/daemon.json配置文件。 sudo vi /etc/docker/daemon.json…

MacOS上配置docker国内镜像仓库地址

背景 docker官方镜像仓库网速较差&#xff0c;我们需要设置国内镜像服务 我的MacOS docker版本如下 设置docker国内镜像仓库地址 点击Settings点击Docker Engine修改配置文件&#xff0c;添加registry-mirrors {"builder": {"gc": {"defaultKeepS…

Docker国内镜像加速地址与详细说明

简介 对于动不动就几百M甚至上G的Docker镜像来说&#xff0c;官方镜像总是掉线或速度极慢&#xff0c;为了改善这种情况&#xff0c;建议切换成国内镜像。常用的国内镜像使用阿里云、网易的居多&#xff0c;本篇内容将记录一下Docker的这些国内镜像是怎么使用的。 国内常用的…

Docker国内镜像

1、下面这些镜像实测基本都用不了。 网易镜像中心&#xff1a;http://hub-mirror.c.163.com daocloud镜像市场&#xff1a;https://hub.daocloud.io 七牛云&#xff1a;https://reg-mirror.qiniu.com Azure&#xff1a;https://dockerhub.azk8s.cn 中科大: https://docke…

docker 国内镜像配置

一、常用镜像地址 1、中国科技大学:https://docker.mirrors.ustc.edu.cn 2、阿里云:https://cr.console.aliyun.com/ 3、Docker中国区官方镜像:https://registry.docker-cn.com 4、网易:http://hub-mirror.c.163.com 5、ustc:https://docker.mirrors.ustc.edu.cn 6、daoclo…

window docker国内镜像设置

刚开始使用的时候&#xff0c;发现因为网络的问题&#xff0c;经常出现镜像下载失败的情况。如下图所示。 这是应为docker服务在国外,直接访问会因为网络原因失败或者特别慢,因此我们可以将将镜像源设置为国内的。 一. 在桌面右下角出有一个docker的图标,鼠标右键点击setings,…

【Docker】Docker 设置国内镜像源_docker国内镜像库

文章目录 1. Docker阿里云镜像加速2. 参考资料 点击跳转&#xff1a;Docker安装MySQL、Redis、RabbitMQ、Elasticsearch、Nacos等常见服务全套&#xff08;质量有保证&#xff0c;内容详情&#xff09; 1. Docker阿里云镜像加速 在国内&#xff0c;从官方的Docker Hub仓库拉取…

docker构建国内镜像服务

在国内想要下载镜像比较困难&#xff0c;因此很多公司都构建自己的私有仓库。如何搭建私有仓库&#xff0c;请参考《docker私有仓库从无到有》。然而即使私有仓库服务构建完成&#xff0c;但是里面没有镜像&#xff0c;一样很苦恼。今天介绍一下如何利用国内云服务商提供的镜像…

电网电压的三相静止对称坐标系和三相电网电压的相量表示法

电网电压的空间电压矢量和电网电压的相量表示这两个概念需要区分清楚。分别参考邱关源的《电路》和张兴的《PWM整流》相关章节。 图2 三相电网电压的相量表示法 电网电压的相量表示&#xff0c;三相相差120度&#xff0c;整体逆时针50HZ旋转&#xff0c;这里的120度是指三分之一…

交流电中为什么要用相量法?

上两节课,电工学了电流和电压的相量表示法,对于复数的引入感觉稀里糊涂的,于是去搜了知乎,一篇文章让我恍然大悟,如果也有不理解的小伙伴可以复制这个,去知乎看详细解答嗷~ https://www.zhihu.com/question/347763932/answer/1103938667 下面👇是我的理解➕概括总结:…

斯泰因梅茨-电路向量法的创始人

施泰因梅茨&#xff08;Steinmetz&#xff0c;Charles Protells&#xff09;德裔美国电机工程师。美国艺术与科学学院院士。1865年4月9日生于德国的布雷斯劳&#xff08;今波兰的弗罗茨瓦夫&#xff09;。1901 &#xff5e;1902 年任美国电机工程师学会主席。1889年迁居美国。他…

相量法

复数 代数形式 三角形式 指数形式 极坐标形式 正弦量 的三要素 峰值&#xff0c;峰峰值 角速度、频率、周期 初相 有效值即 相位差 相量法的基础 则 则 电路定律的相量形式 则 则 则

数字正交下变频(多相滤波法)

[TOC] 多相滤波原理 在上一篇文章中讨论了基于低通滤波方式的正交下变频系统。下面是该种方式的结构图。 图1.1 数字正交下变频 但是,这种典型的数字下变频方式也存在着这样一些缺陷: * 当采样信号为宽带信号时,需要较高的采样频率,此时会带来系统设计的复杂度; * 先滤…

综合函数矩量法原理及实现思路

0引言 前两篇博客我们介绍了基于RWG函数的三维矩量法的基本原理和其对应的代码实现&#xff08;源代码已上传本站&#xff0c;正在审核中&#xff09;。 矩量法作为最早提出的经典数值算法之一&#xff0c;以较高的计算精度和对任意形状目标良好的适应性而被广大学者所偏爱。从…

结点电压法

结点电压的概念 任选电路中某一结点为零电位参考点(用 ⊥表示)&#xff0c;其它各结点对参考点的电压&#xff0c;称为结点电压。 结点电压法适用于支路数较多, 结点数较少的电路。 结点电压的参考方向&#xff1a;从结点指向参考结点。 结点电压法&#xff1a;以结点电压为…

Matlab+cpp矩量法代码演示

Matlabcpp矩量法代码演示 0前言1三维目标几何剖分与网格信息处理2阻抗矩阵计算3激励矩阵填充与表面电流求解4根据目标表面电流计算空间散射场及RCS5示例代码与说明 0前言 在上一篇博客中&#xff0c;我们详细介绍了矩量法&#xff08;MoM&#xff09;的原理及其数值求解过程。…