数据结构:跳表讲解

跳表

    • 1.什么是跳表-skiplist
      • 1.1简介
      • 1.2设计思路
    • 2.跳表的效率分析
    • 3.跳表实现
      • 3.1类成员设计
      • 3.2查找
      • 3.3插入
      • 3.4删除
      • 3.5完整代码
    • 4.skiplist跟平衡搜索树和哈希表的对比

1.什么是跳表-skiplist

1.1简介

skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。后面我会进行比对。

skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A
Probabilistic Alternative to Balanced Trees》。

skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。如果是一个有序的链表,查找数据的时间复杂度是O(N)。


1.2设计思路


(1)假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图所示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半(后面讲为什么)

在这里插入图片描述
这样设计利于查找,查找规则为:

  1. cur表示当前节点,nextV为节点指针数组,j表示下标,key为要查找的值。 其中cur一开始为哨兵节点,j为顶部下标
  2. 从cur位置向右看,如果key大于右边,就直接向右走,即更新cur为右节点
    (即cur = cur->nextV[j])。
  3. 如果key小于右边或者右边为空,直接向下走,即让j减一
  4. 右边等于key找到。
  5. 不存在的情况,j最后会走到-1(看后面图解)。

查找存在的情况:
在这里插入图片描述

查找不存在的情况:
在这里插入图片描述


(2)以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。如下图,这样搜索效率就进一步提高了。

在这里插入图片描述


(3)skiplist正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似二分查找,使得查找的时间复杂度可以降低到O(log n)。但是这个结构在插入删除数据的时候有很大的问题,插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新退化成O(n)


(4)skiplist的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数,这样就好处理多了。细节过程入下图(插入和删除过程后面详细讲,现在只需知道随机层数一样可行):

在这里插入图片描述



2.跳表的效率分析

上面我们说到,skiplist插入一个节点时随机出一个层数,听起来怎么这么随意,如何保证搜索时的效率呢?

这里首先要细节分析的是这个随机层数是怎么来的。一般跳表会设计一个最大层数maxLevel的限制,其次会设置一个多增加一层的概率p。那么计算这个随机层数的代码如下:

int RandomLevel()
{int level = 1;//RAND_MAX为rand函数可生成的最大值//即rand()落到[0, RAND_MAX * _p]的概率为_pwhile(rand() < RAND_MAX * _p){level++;}return level;}

根据前面RandomLevel(),我们很容易看出,产生越高的节点层数,概率越低。定量的分析如下:

  • 节点层数至少为1。而大于1的节点层数,满足一个概率分布。
  • 节点层数恰好等于1的概率为1-p(即第一次就失败)。
  • 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)(第一次成功,第二次失败)。
  • 节点层数大于等于3的概率为p^2, 而节点层数恰好等于3的概率为p^2*(1-p)。
  • 节点层数大于等于4的概率为p^3, 而节点层数恰好等于4的概率为p^3*(1-p)。
  • ……

因此,一个节点的平均层数(也即包含的平均指针数目),计算如下:
在这里插入图片描述

现在很容易计算出:

  • 当p=1/2时,每个节点所包含的平均指针数目为2;
  • 当p=1/4时,每个节点所包含的平均指针数目为1.33。
  • p越大,平均层数越多,时间效率就越快,但太大可能导致空间浪费,故一般都会限制最大层数。

跳表的平均时间复杂度为O(logN),我会用图解来帮助大家理解大概,但完整推导的过程较为复杂,需要有一定的数学功底,有兴趣的同学,可以参考以下文章中的讲解:

铁蕾大佬的博客:Redis内部数据结构详解(6)——skiplist

在这里插入图片描述



3.跳表实现

本文跳表实现以本题为准:设计跳表

3.1类成员设计

//跳表节点
struct SkiplistNode
{SkiplistNode(int val, int level){_val = val;_nextV.resize(level, nullptr); }int _val;  //节点值vector<SkiplistNode*> _nextV;  //指针数组
};//跳表
class Skiplist {
public:typedef SkiplistNode Node;Skiplist() {srand(time(0));  //设置随机数种子_head = new Node(-1, 1);}double _p = 0.25;  //增加层数的概率int _maxLevel = 32;  //限制最大层数Node* _head;  //哨兵头节点,存储的是无效数据//头节点从一层开始,后面生成了更高层数节点在扩容,减少不必要的查询工作
};

3.2查找

参照设计思路里面讲的即可

bool search(int target) 
{int level = _head->_nextV.size() - 1;  //下标从顶部开始Node* cur = _head;  //从哨兵位开始while(level >= 0){//大于,跳到下个节点//小于或者空,向下走if(cur->_nextV[level] && cur->_nextV[level]->_val < target)  {cur = cur->_nextV[level];}else if(!cur->_nextV[level] || cur->_nextV[level]->_val > target)  {level--;}else  //找到了{return true;}}return false;
}

3.3插入

思路很简单,假设当前插入节点有x层,只需要找到这x层每一层对应的前置节点,然后做简单的链接工作即可。
在这里插入图片描述

//找前置节点
vector<SkiplistNode*> GetPrev(int num)
{//核心是找到每一层的前置节点//本题允许冗余int level = _head->_nextV.size() - 1;vector<SkiplistNode*> prevV(level + 1, nullptr);Node* cur = _head;while(level >= 0){//大于,跳到下个节点//小于或者空,向下走if(cur->_nextV[level] && cur->_nextV[level]->_val < num)  {cur = cur->_nextV[level];}else  {prevV[level] = cur;level--;}}return prevV;
}//插入节点
void add(int num) {vector<SkiplistNode*> prevV = GetPrev(num);//生成节点int n = RandomLevel();Node* newnode = new Node(num, n);if(n > _head->_nextV.size())   //节点层数超过当前最大{_head->_nextV.resize(n, nullptr);prevV.resize(n, _head);}//链接节点for(int i = 0; i < n; i++){newnode->_nextV[i] = prevV[i]->_nextV[i];prevV[i]->_nextV[i] = newnode;}
}

3.4删除

删除大体分两种情况:

  1. 存在该节点,思路与插入类似,假设当前删除节点有x层,只需要找到这x层每一层对应的前置节点,然后做简单的链接工作即可。
  2. 不存在该节点,那找前置节点的时候第0层的右边要么是空,要么就不是目标值

在这里插入图片描述

删除还有个可优化的点,不做也不影响正确性:
在这里插入图片描述

bool erase(int num) {vector<SkiplistNode*> prevV = GetPrev(num);//随机生成至少有一层节点if(prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)  //不存在{return false;}else {//记录待删除的节点SkiplistNode* del = prevV[0]->_nextV[0];for(int i = 0; i < del->_nextV.size(); i++){prevV[i]->_nextV[i] = del->_nextV[i];}delete del;//这里不影响正确性,对头节点的多余空间做处理int j = _head->_nextV.size() - 1;while(j >= 0){if(_head->_nextV[j] == nullptr){j--;}else{break;}}_head->_nextV.resize(j + 1);return true;}
}

3.5完整代码

struct SkiplistNode
{SkiplistNode(int val, int level){_val = val;_nextV.resize(level, nullptr); }int _val;vector<SkiplistNode*> _nextV;
};class Skiplist {
public:typedef SkiplistNode Node;Skiplist() {srand(time(0));_head = new Node(-1, 1);}bool search(int target) {int level = _head->_nextV.size() - 1;Node* cur = _head;while(level >= 0){//大于,跳到下个节点//小于或者空,向下走if(cur->_nextV[level] && cur->_nextV[level]->_val < target)  {cur = cur->_nextV[level];}else if(!cur->_nextV[level] || cur->_nextV[level]->_val > target)  {level--;}else  //找到了{return true;}}return false;}vector<SkiplistNode*> GetPrev(int num){//核心是找到每一层的前置节点//本题允许冗余int level = _head->_nextV.size() - 1;vector<SkiplistNode*> prevV(level + 1, nullptr);Node* cur = _head;while(level >= 0){//大于,跳到下个节点//小于或者空,向下走if(cur->_nextV[level] && cur->_nextV[level]->_val < num)  {cur = cur->_nextV[level];}else  {prevV[level] = cur;level--;}}return prevV;}void add(int num) {vector<SkiplistNode*> prevV = GetPrev(num);//链接节点int n = RandomLevel();Node* newnode = new Node(num, n);if(n > _head->_nextV.size()){_head->_nextV.resize(n, nullptr);prevV.resize(n, _head);}for(int i = 0; i < n; i++){newnode->_nextV[i] = prevV[i]->_nextV[i];prevV[i]->_nextV[i] = newnode;}}bool erase(int num) {vector<SkiplistNode*> prevV = GetPrev(num);//随机生成至少有一层节点if(prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)  //不存在{return false;}else {//记录待删除的节点SkiplistNode* del = prevV[0]->_nextV[0];for(int i = 0; i < del->_nextV.size(); i++){prevV[i]->_nextV[i] = del->_nextV[i];}delete del;//这里不影响正确性,对头节点的多余空间做处理int j = _head->_nextV.size() - 1;while(j >= 0){if(_head->_nextV[j] == nullptr){j--;}else{break;}}_head->_nextV.resize(j + 1);return true;}}int RandomLevel(){int level = 1;while(rand() < RAND_MAX * _p){level++;}return level;}double _p = 0.25;int _maxLevel = 32;Node* _head;
};



4.skiplist跟平衡搜索树和哈希表的对比

  1. skiplist相比平衡搜索树(AVL树和红黑树)对比,都可以做到遍历数据有序,时间复杂度也差不多
    skiplist的优势是:
    a、skiplist实现简单,容易控制。平衡树增删查改遍历都更复杂。
    b、skiplist的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子/颜色等消耗。skiplist中p=1/2时,每个节点所包含的平均指针数目为2;skiplist中p=1/4时,每个节点所包含的平均指针数目为1.33;
  2. skiplist相比哈希表而言,就没有那么大的优势了哈希表平均时间复杂度是O(1),比skiplist快。。
    skiplist优势如下:
    a、遍历数据有序b、skiplist空间消耗略小一点,哈希表存在链接指针和表空间消耗。
    b、哈希表扩容有性能损耗。
    c、哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力,实现复杂。

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

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

相关文章

Python开发户型图编辑器-2D/3D户型图展示

在现代家居设计中&#xff0c;户型图是不可或缺的工具&#xff0c;它为设计师和业主提供了一个直观的展示和规划空间的方式。然而&#xff0c;传统的户型图编辑软件往往复杂难用&#xff0c;限制了设计师的创作灵感。我们为您带来了一款全新的Python开发的户型图编辑器&#xf…

计算机视觉基础:【矩阵】矩阵选取子集

OpenCV的基础是处理图像&#xff0c;而图像的基础是矩阵。 因此&#xff0c;如何使用好矩阵是非常关键的。 下面我们通过一个具体的实例来展示如何通过Python和OpenCV对矩阵进行操作&#xff0c;从而更好地实现对图像的处理。 示例 示例&#xff1a;选取矩阵中指定的行和列的…

Positive Technologies 确保 Rostic‘s 网络应用程序的安全

☑️ PT BlackBox分析 Rostics 网络应用程序的安全性 快餐连锁店在其安全网络开发过程中使用了我们的扫描仪。PT BlackBox 总共扫描了 20 多个 Rostics 的外部服务&#xff08;每天访问量超过 100,000 人次&#xff09;和企业服务&#xff08;每天访问量≈7,000 名员工&#x…

区块链游戏解说:什么是 Nine Chronicles

作者&#xff1a;lesleyfootprint.network 编译&#xff1a;cicifootprint.network 数据源&#xff1a; Nine Chronicles Dashboard 什么是 Nine Chronicles Nine Chronicles 是一款去中心化的在线角色扮演游戏&#xff0c;标志着在线游戏和区块链技术的发展。 Nine Chroni…

小米14 ULTRA:重新定义手机摄影的新篇章

引言 随着科技的飞速发展&#xff0c;智能手机已经不仅仅是一个通讯工具&#xff0c;它更是我们生活中的一位全能伙伴。作为科技领域的佼佼者&#xff0c;小米公司再次引领潮流&#xff0c;推出了全新旗舰手机——小米14 ULTRA。这款手机不仅在性能上进行了全面升级&am…

基于springboot+vue医院电子病历管理系统

一、系统架构 前端&#xff1a;vue | element-ui 后端&#xff1a;springboot | mybatis 环境&#xff1a;jdk1.8 | mysql | maven | node 二、 代码及数据库 三、功能介绍 01. 登录页 02. 用户管理-管理员 03. 用户管理-患者 04. 用户管理-医生 05. 用户管理…

如何实现上拉加载,下拉刷新?

文章目录 一、前言二、实现原理上拉加载下拉刷新 三、案例小结 参考文献 一、前言 下拉刷新和上拉加载这两种交互方式通常出现在移动端中 本质上等同于PC网页中的分页&#xff0c;只是交互形式不同 开源社区也有很多优秀的解决方案&#xff0c;如iscroll、better-scroll、pu…

每日一题——LeetCode1470.重新排列数组

方法一 把数组的前n项看做一个数组&#xff0c;后n项看做一个数组&#xff0c;两个数组循环先后往res里push元素 var shuffle function(nums, n) {let res[]for(let i0;i<n;i){res.push(nums[i])res.push(nums[in])}return res }; 消耗时间和内存情况&#xff1a; 方法二…

【sgCreateTableData】自定义小工具:敏捷开发→自动化生成表格数据数组[基于el-table]

源码 <template><!-- 前往https://blog.csdn.net/qq_37860634/article/details/136141769 查看使用说明 --><div :class"$options.name"><div class"sg-head">表格数据生成工具</div><div class"sg-container&quo…

multipath多路径显示异常

一、现象 使用multipath -ll显示一个磁盘多大3条路径都是active faulty running状态&#xff0c;而正常状态的链路应该是&#xff1a;active ready running&#xff0c;具体如下图&#xff1a; 二、分析 1、通过各种检查均未发现异常&#xff0c;检查内容包括&#xff1a;操作…

刷LeetCode541引起的java数组和字符串的转换问题

起因是今天在刷下面这个力扣题时的一个报错 541. 反转字符串 II - 力扣&#xff08;LeetCode&#xff09; 这个题目本身是比较简单的&#xff0c;所以就不讲具体思路了。问题出在最后方法的返回值处&#xff0c;要将字符数组转化为字符串&#xff0c;第一次写的时候也没思考直…

【经验分享】自然语言处理技术有哪些局限性和挑战?

个人认为&#xff0c;主要是两个难点&#xff1a; 1.语料&#xff0c;通常的语料很好解决&#xff0c;用爬虫从互联网上就可以采集和标注训练。但是我们接触很多项目和客户需求都是专业性很强的&#xff0c;例如&#xff1a;航天材料、电气设备、地理信息、化学试剂 等等。往往…

【微服务生态】Docker

文章目录 一、基础篇1. 简介2. 下载与安装3. 常用命令3.1 帮助启动类3.2 镜像命令3.3 容器命令 4. Docker 容器数据券5. Docker 镜像5.1 commit 生成镜像5.2 Docker Registry5.3 发布镜像 6. Docker 常规安装软件 二、高级篇1. Dockerfile1.1 概述1.2 基础知识1.3 Dockerfile常…

如何系统地自学Python?

如何系统地自学Python&#xff1f; 如何系统地自学Python&#xff1f;1.了解编程基础2.学习Python基础语法3.学习Python库和框架4.练习编写代码5.参与开源项目6.加入Python社区7.利用资源学习8.制定学习计划9.持之以恒总结 如何系统地自学Python&#xff1f; 作为一个Python语…

32单片机基础:GPIO输出

目录 简介&#xff1a; GPIO输出的八种模式 STM32的GPIO工作方式 GPIO支持4种输入模式&#xff1a; GPIO支持4种输出模式&#xff1a; 浮空输入模式 上拉输入模式 下拉输入模式 模拟输入模式&#xff1a; 开漏输出模式&#xff1a;&#xff08;PMOS无效&#xff0c;就…

三防加固平板在房地产行业的应用|亿道三防onerugged

近期&#xff0c;有一款引人注目的解决方案——亿道三防onerugged平板电脑&#xff0c;它以其出色的性能和多功能的设计&#xff0c;为房地产行业带来了全新的应用体验。 首先&#xff0c;亿道三防onerugged平板电脑的NFC功能在小区业主身份验证中发挥着重要作用。传统的身份验…

vue3组件通信方式汇总

前言&#xff1a;本文默认读者有JS基础和Vue基础&#xff0c;如果没有这个两个基础&#xff0c;可能阅读比较困难&#xff0c;建议先看下官方文档&#xff0c;当然&#xff0c;也欢迎评论交流&#x1f601; 通信方式总结 常见搭配形式 一、props&#xff08;使用频率最高&#…

HarmonyOS 鸿蒙应用开发(十二、paho-mqtt的mqtt客户端库移植)

PAHO MQTT 是 Eclipse Foundation 的一个开源项目&#xff0c;提供了用于 MQTT 协议的客户端库。这个库为使用 MQTT 协议的应用程序提供了丰富的功能和灵活性。轻量级mqtt服务器mosquitto也是其中有名的一个开源项目。 目录 PAHO MQTT介绍 移植过程 index.d.ts文件修改 写…

SOPHON算能科技新版SDK环境配置以及C++ demo使用过程

目录 1 SDK大包下载 2 获取SDK中的库文件和头文件 2.1 注意事项 2.2 交叉编译环境搭建 2.2.1 首先安装工具链 2.2.2 解压sophon-img包里的libsophon_soc__aarch64.tar.gz&#xff0c;将lib和include的所有内容拷贝到soc-sdk文件夹 2.2.3 解压sophon-mw包里的sophon-mw-s…

hadoop FileSystem是否要close

先来说结论&#xff0c;最好不要close&#xff0c;让hadoop自己close&#xff0c;否则容易把进程里其他获取fs的地方一起关了。这是因为 FileSystem.get(Configuration)做了缓存的原因。当然可以设置 conf.setBoolean("fs.hdfs.impl.disable.cache", true); 就不缓存…