跳表 skiplist 简介

article/2025/8/8 0:10:48

跳表 skiplist

跳表 (Skip List) 是由 William Pugh 在 1990 年发表的文章 Skip Lists: A Probabilistic Alternative toBalanced Trees 中描述的一种查找数据结构,支持对数据的快速查找,插入和删除。

对于 AVL 树、红黑树等平衡树,在插入过程中需要做多次旋转操作,实现复杂,而跳表实现更简单、开销更低,应用广泛。

在这里插入图片描述
跳表是一个包含随机算法的数据结构,跳表的查找、插入、删除的平均时间复杂度都是 O(logn),而且可以按照范围区间查找元素,空间复杂度是 O(n)。

跳表的最底层通常是一个有序链表,上层是下层的一个索引,下层元素出现在上层的概率为 p (通常 p=1/2)。

跳表的创建过程

首先,对于一个有序链表,如果我们需要查找元素,则需要从头开始遍历链表,查找时间复杂度为 O(n)。如果我们每相邻两个节点增加一个指针,让指针指向下下个节点,那么上层形成了一个减缩版的链表,当我查找元素时,先在上层链表查找,当待查元素在两个节点之间时,再回到下层链表查找,这样我们就可以跳过一些节点,提高查找速度。以此类推,我们可以在第二层链表上建立第三层链表……查找一个元素时,从最高层开始,逐层向下,直到找到为止。

在这里插入图片描述
上述我们建立的数据结构就是一个跳表,我们可以将上层链表看作是下层链表的一个索引,每相邻两个、三个……建立一个索引,这是一种确定性策略,如果只是查找操作,这么建立跳表没有问题。但是当我们需要频繁插入和删除元素时,这种确定性策略会使维护跳表变得复杂。

为了解决上述问题,我们引入随机策略,不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机产生一个层数(level)。

// 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 :
//        1/2 的概率返回 1
//        1/4 的概率返回 2
//        1/8 的概率返回 3 以此类推
private int randomLevel() {int level = 1;// 当 level < MAX_LEVEL,且随机数小于设定的晋升概率时,level + 1while (Math.random() < SKIPLIST_P && level < MAX_LEVEL)level += 1;return level;
}

创建跳表我们首先需要一个返回层数的随机函数 randomlevel(),在有序链表的每一个元素上运行该随机函数,当返回 level=1 时,不创建索引;当返回 level=2 时,对该元素创建一级索引;当返回 level=3 时,对该元素创建一级和二级索引……

如果 SKIPLIST_P=1/2 时,一级索引有 n/2 个元素,二级索引有 n/4 个元素……空间复杂度为 O(n)。如果底层链表中的元素存储的是对象,索引只需存储对象排序的键值 key 即可。

查找元素

在这里插入图片描述
查找元素时,从最高层索引开始查找,逐层向下,直到找到为止。根据概率可以证明查找的平均时间复杂度为 O(logn)。

V& find(const K& key) {SkipListNode<K, V>* p = head;// 找到该层最后一个键值小于 key 的节点,然后走向下一层for (int i = level; i >= 0; --i) {while (p->forward[i]->key < key) {p = p->forward[i];}}// 现在是小于,所以还需要再往后走一步p = p->forward[0];// 成功找到节点if (p->key == key) return p->value;// 节点不存在,返回 INVALIDreturn tail->value;
}

插入元素

在这里插入图片描述
插入元素的关键是查找元素的合适插入位置,将元素插入链表中之后,运行 randomlevel 函数,确定在该元素上建立几层索引。

void insert(const K &key, const V &value) {// 用于记录需要修改的节点SkipListNode<K, V> *update[MAXL + 1];SkipListNode<K, V> *p = head;for (int i = level; i >= 0; --i) {while (p->forward[i]->key < key) {p = p->forward[i];}// 第 i 层需要修改的节点为 pupdate[i] = p;}p = p->forward[0];// 若已存在则修改if (p->key == key) {p->value = value;return;}// 获取新节点的最大层数int lv = randomLevel();if (lv > level) {lv = ++level;update[lv] = head;}// 新建节点SkipListNode<K, V> *newNode = new SkipListNode<K, V>(key, value, lv);// 在第 0~lv 层插入新节点for (int i = lv; i >= 0; --i) {p = update[i];newNode->forward[i] = p->forward[i];p->forward[i] = newNode;}++length;
}

删除元素

在这里插入图片描述
删除元素的关键同样是查找操作,先找到待删除元素的位置,然后删除对应元素及其索引,删除操作调整对应指针即可。

bool erase(const K &key) {// 用于记录需要修改的节点SkipListNode<K, V> *update[MAXL + 1];SkipListNode<K, V> *p = head;for (int i = level; i >= 0; --i) {while (p->forward[i]->key < key) {p = p->forward[i];}// 第 i 层需要修改的节点为 pupdate[i] = p;}p = p->forward[0];// 节点不存在if (p->key != key) return false;// 从最底层开始删除for (int i = 0; i <= level; ++i) {// 如果这层没有 p 删除就完成了if (update[i]->forward[i] != p) {break;}// 断开 p 的连接update[i]->forward[i] = p->forward[i];}// 回收空间delete p;// 删除节点可能导致最大层数减少while (level > 0 && head->forward[level] == tail) --level;// 跳表长度--length;return true;
}

复杂度证明

空间复杂度

对于一个节点而言,节点的层数为 i i i 的概率为 p i − 1 ( 1 − p ) p^{i-1}(1-p) pi1(1p),则该节点的期望层数为:
∑ i ≥ 1 , p < 1 i p i − 1 ( 1 − p ) = 1 1 − p \sum_{i\ge 1,p<1}ip^{i-1}(1-p)=\frac{1}{1-p} i1,p<1ipi1(1p)=1p1
则跳表的期望空间为 n 1 − p \frac{n}{1-p} 1pn,且因为 p p p 为常数,所以跳表的期望空间复杂度为 O(n)。在最坏的情况下,每一层有序链表等于初始有序链表,即跳表的最差空间复杂度为 O(nlogn)。

时间复杂度

从后向前分析查找路径,这个过程可以分为从最底层爬到第 L ( n ) L(n) L(n) 层和后续操作两个部分。在分析时,假设一个节点的具体信息在它被访问之前是未知的。

假设当前我们处于一个第 i i i 层的节点 x x x,我们并不知道 x x x 的最大层数和 x x x 左侧节点的最大层数,只知道 x x x 的最大层数至少为 i i i。如果 x x x 的最大层数大于 i i i,那么下一步应该是向上走,这种情况的概率为 p p p;如果 x x x 的最大层数等于 i i i,那么下一步应该是向左走,这种情况概率为 1 − p 1-p 1p

C ( i ) C(i) C(i) 为在一个无限长度的跳表中向上爬 i i i 层的期望代价,定义 C ( 0 ) = 0 C(0)=0 C(0)=0,那么有:
C ( i ) = ( 1 − p ) ( 1 + C ( i ) ) + p ( 1 + C ( i − 1 ) ) C(i)=(1-p)(1+C(i))+p(1+C(i-1)) C(i)=(1p)(1+C(i))+p(1+C(i1))
解得 C ( i ) = i p C(i)=\frac{i}{p} C(i)=pi

由此可以得出:在长度为 n n n 的跳表中,从最底层爬到第 L ( n ) L(n) L(n) 层的期望步数存在上界 L ( n ) − 1 p \frac{L(n)-1}{p} pL(n)1

现在只需要分析爬到第 L ( n ) L(n) L(n) 层后还要再走多少步。易得,到了第 L ( n ) L(n) L(n) 层后,向左走的步数不会超过第 L ( n ) L(n) L(n) 层及更高层的节点数总和,而这个总和的期望为 1 p \frac{1}{p} p1。所以到了第 L ( n ) L(n) L(n) 层后向左走的期望步数存在上界 1 p \frac{1}{p} p1。同理,到了第 L ( n ) L(n) L(n) 层后向上走的期望步数存在上界 1 p \frac{1}{p} p1

所以,跳表查询的期望查找步数为 L ( n ) − 1 p + 2 p \frac{L(n)-1}{p}+\frac{2}{p} pL(n)1+p2,又因为 L ( n ) = l o g 1 p n L(n)=log_{\frac{1}{p}}n L(n)=logp1n,所以跳表查询的期望时间复杂度为 O(logn)。

在最坏的情况下,每一层有序链表等于初始有序链表,查找过程相当于对最高层的有序链表进行查询,即跳表查询操作的最差时间复杂度为 O(n)。

插入操作和删除操作就是进行一遍查询的过程,途中记录需要修改的节点,最后完成修改。易得每一层至多只需要修改一个节点,又因为跳表期望层数为 l o g 1 p n log_{\frac{1}{p}}n logp1n,所以插入和修改的期望时间复杂度也为 O(logn)。

skiplist 与平衡树、哈希表的比较

  • skiplist 和各种平衡树(如 AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个 key 的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
  • 在做范围查找的时候,平衡树比 skiplist 操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在 skiplist 上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
  • 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而 skiplist 的插入和删除只需要修改相邻节点的指针,操作简单又快速。
  • 从内存占用上来说,skiplist 比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而 skiplist 每个节点包含的指针数目平均为 1 / ( 1 − p ) 1/(1-p) 1/(1p),具体取决于参数 p p p 的大小。如果像 Redis 里的实现一样,取 p = 1 / 4 p=1/4 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
  • 查找单个 key,skiplist 和平衡树的时间复杂度都为 O(logn),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近 O(1),性能更高一些。所以我们平常使用的各种 Map 或 dictionary 结构,大都是基于哈希表实现的。
  • 从算法实现难度上来比较,skiplist 比平衡树要简单得多。

参考文献

[1] Skip Lists: A Probabilistic Alternative toBalanced Trees
[2] Skip list - Wikipedia
[3] Redis内部数据结构详解(6)——skiplist
[4] 跳表 - OI Wiki


http://chatgpt.dhexx.cn/article/euIad8g7.shtml

相关文章

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;的原理及其数值求解过程。…

迭代重心法 matlab,重心法

重心法(The centre-of-gravity method) [编辑] 什么叫重心法? 重心法(The centre-of-gravity method)是一种设置单个厂房或仓库的方法,这种方法主要考虑的因素是现有设施之间的距离和要运输的货物量,经常用于中间仓库或分销仓库的选择。商品运输量是影响商品运输费用的主要因…