DS高阶:跳表

一、skiplist

1.1 skiplist的概念

        skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》 

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


1.2 skiplist的优化思路分析

           假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图b所
示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半。(多层链表的启发思路)以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。如下图c,这样搜索效率就进一步提高了。

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

       skiplist的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是
插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数
,这样就好处理多了。

 1.3 随机出层数的含义

         插入节点时随机出一个层数究竟是什么意思呢???难道直接random任意数就可以了吗??

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

 一个节点的平均层数(也即包含的平均指针数目),计算如下:

现在很容易计算出:
当p=1/2时,每个节点所包含的平均指针数目为2;
当p=1/4时,每个节点所包含的平均指针数目为1.33。

时间复杂度:logN

具体的分析可以看下面的文章:Redis内部数据结构详解(6)——skiplist
 

2、skiplist的模拟实现

力扣有一道设计跳表的题. - 力扣(LeetCode)设计跳表

基本的调表需要实现4个函数:构造函数、搜索、插入、删除。下面我们来一个个分析。

2.1 skiplist的基本结构

struct SkiplistNode
{int _val;//存储对应的值vector<SkiplistNode*> _nextV;//存放对应的next指针集合SkiplistNode(const int&val, size_t level = 1) //level表示需要开辟的层数 不传就是默认开满:_val(val){_nextV.resize(level, nullptr);}
};class Skiplist
{typedef  SkiplistNode Node;
public:private:Node* _head;//虚拟头节点const size_t _maxLevel = 32; //用缺省参数去初始化const double _p = 0.25;//用缺省参数去初始化
};

 2.2 skiplist的默认构造

Skiplist()
{srand((unsigned int)time(nullptr));//为了方便后面的随机取层数,先弄一个随机种子_head = new Node(-1);//默认开一层,用默认构造初始化
}

        给虚拟头节点申请一块空间,一开始默认就开一层。为了能够方面后面利用rand函数随机取层数,所以在这个地方先用了一个时间种子

         我们默认开的是一层,因为在数据量小的时候其实我们可以根据插入的情况去调整_head的层数,如果是数据量特别大的话,也可以一次性就把他开到满

2.3 skiplist的搜索

bool search(int target) 
{//要不断往下走Node* cur = _head;int level = _head->_nextV.size() - 1;//从后往前去找while (level >= 0){//如果我比你大 我就跳过去->更新cur   //如果我比你小或者你为空 我就往下走 --levelif (cur->_nextV[level] == nullptr || target < cur->_nextV[level]->_val) --level;else if (target > cur->_nextV[level]->_val)  cur = cur->_nextV[level];else return true;}return false; //循环结束都没有找到,说明找不到。
}

我们要从高层一直找到底层,所以要从_nextV的后面开始找。

1、如果你为空,或者我比你小,那就得往下走 ->--level

2、如果我比你大,就可以直接跳到你的位置->更新cur=cur->_nextV[level]

3、如果找到了就返回true,如果循环结束了都找不到,那就返回false

2.4 找到prevV指针数组

为什么要单独去封装这个函数呢?

         因为不管是插入,还是删除,我们都需要去找前驱节点的集合,这样才能去改变连接关系,所以为了提高代码的复用性,封装这样的一个函数,去找到待插入位置或者是待删除位置的前驱节点集合。

vector<Node*> FindPrevNode(int num) //帮助我们找到前驱指针集合
{//最终我们要返回待插入位置或者是待删除位置的前驱指针集合  一开始的时候默认是head、Node* cur = _head;int level = _head->_nextV.size() - 1;vector<Node*> prevV(level+1, _head);while (level >= 0){if (cur->_nextV[level] == nullptr || num < cur->_nextV[level]->_val){//更新level的层的前一个节点 往下跳之前保存前驱节点prevV[level] = cur;--level;}else//(num >= cur->_nextV[level]->_val)  cur = cur->_nextV[level];}return prevV;
}

        当我们需要往后面跳之前,保存当前的cur进去prevV数组中,这样我们返回的数组就是待插入节点对应的前驱节点集合了!

2.5 随机层数的生成函数

      我们在插入节点之前,要随机生成一个层数,所以要先实现一个生成层数的函数

2.5.1 C语言rand( )版本

size_t RandomLevel() //C语言版本
{size_t level = 1;//初始的层数while (rand() <= RAND_MAX * _p && level < _maxLevel)  ++level; //RAND_MAX是随机数的最大值return level;
}

2.5.2 C++11随机数库

	size_t RandomLevel() //需要的时候去搜 C++11的随机数库即可  头文件chrono和random{//类似随机数种子,但是只用一次是最好的 所以设置成staic 这样就只会调用一次了static std::default_random_engine generator(std::chrono::system_clock::now().time_since_epoch().count());//now.time_since_epoch().count()是一个时间戳 类似随机数种子static std::uniform_real_distribution<double> distribution(0.0, 1.0);size_t level = 1;while (distribution(generator) <= _p && level < _maxLevel)  ++level;return level;}

         std::chrono::system_clock::now().time_since_epoch().count() 类似一个时间戳,相当于是随机种子,但是由于只需要初始化一次,所以我们将他变成static变量,这样就只要初始化一次即可!

        关于C++11的random库用法,还是比较复杂的,大家可以参考一些相关的文章。

2.6 skiplist的增加

void add(int num)  //插入节点
{vector<Node*> prevV = FindPrevNode(num); //右值引用size_t n = RandomLevel(); //表示需要开多少层//如果n超过了_head的最大层数,那么就要调整一下if (n > _head->_nextV.size()){_head->_nextV.resize(n, nullptr); prevV.resize(n, _head);//不够的地方也要更新过去}Node* newnode = new Node(num, n);//申请对应的新节点  然后根据prevV数组去建立连接for (size_t i = 0; i < n; ++i) //连接前后节点,首先要先连后面的 再连前面的{newnode->_nextV[i] = prevV[i]->_nextV[i];prevV[i]->_nextV[i] = newnode;}
}

         一个很关键的地方就是,我们随机生成了一个层数后,有可能我们的_head的层数都没这个多,所以我们必须利用resize去初始化一下,否则会出现越界访问。 

         中间插入的逻辑就类似链表的指定位置插入,先让自己的后继指向前驱的后继,然后再让前驱指向自己,必须按照这个顺序,否则会丢失节点

2.7 skiplist的删除

bool erase(int num) 
{//首先 有可能没有这个数 所以要看看是不是真的没有vector<Node*> prevV = FindPrevNode(num);if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)  return false;//有的话,就要去删除然后重新连接Node* del = prevV[0]->_nextV[0];//我们需要删除的节点,但是在删除前要调整一下连接的关系for (size_t i = 0; i < del->_nextV.size(); ++i)  prevV[i]->_nextV[i] = del->_nextV[i];delete del;// 如果删除最高层节点,把头节点的层数也降一下int i = _head->_nextV.size() - 1;while (i >= 0){if (_head->_nextV[i] == nullptr)  --i;else  break;}_head->_nextV.resize(i + 1);return true;
}

     有可能我们找不到这个数,这个时候就没什么可以删的了。

      在删除这个节点之前,我们要先记录这个节点,然后去改变被删除节点的连接关系,类似链表的指定位置删除。

      如果我们删除的恰好是最高层的节点,这个时候可以整体对头结点的层数降个高度,这样就提高了查找效率。

三、skiplist跟平衡搜索树和哈希表的对比

1. skiplist相比平衡搜索树(AVL树和红黑树)对比,都可以做到遍历数据有序,时间复杂度也差不多。但是skiplist在平衡树面前优势明显。

skiplist的优势是:

a、skiplist实现简单,容易控制。平衡树增删查改遍历都更复杂。

b、skiplist的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子/颜色等消耗skiplist中p=1/2时,每个节点所包含的平均指针数目为2;skiplist中p=1/4时,每个节点所包含的平均指针数目为1.33;


2. skiplist相比哈希表而言,就没有那么大的优势了:

哈希表的优势如下:

a、哈希表平均时间复杂度是O(1),比skiplist快。

b、哈希表空间消耗略多一点。

skiplist优势如下:

a、遍历数据有序

b、skiplist空间消耗略小一点,哈希表存在链接指针和表空间消耗。

c、哈希表扩容有性能损耗。

d、哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力。
 

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

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

相关文章

Python学习之路 | Python基础语法(一)

数据类型 Python3 中常见的数据类型有&#xff1a; Number&#xff08;数字&#xff09;String&#xff08;字符串&#xff09;bool&#xff08;布尔类型&#xff09;List&#xff08;列表&#xff09;Tuple&#xff08;元组&#xff09;Set&#xff08;集合&#xff09;Dict…

【Image captioning】基于检测模型网格特征提取——以Sydeny为例

【Image captioning】基于检测模型网格特征提取——以Sydeny为例 今天,我们将重点探讨如何利用Faster R-CNN检测模型来提取Sydeny数据集的网格特征。具体而言,这一过程涉及通过Faster R-CNN模型对图像进行分析,进而抽取出关键区域的特征信息,这些特征在网格结构中被系统地…

代码随想录--链表--反转链表

题目 题意&#xff1a;反转一个单链表。 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL 思路 如果再定义一个新的链表&#xff0c;实现链表元素的反转&#xff0c;其实这是对内存空间的浪费。 其实只需要改变链表的next指针的…

GPU学习记一下线程分组相关

在compute的时候&#xff0c;是要dispatch一个数量的代表分了多少块任务集&#xff0c;dispatch的块内部也是有一个数量的&#xff0c;那么这些值怎么取的呢 内部&#xff0c;N卡32 外面dispatch的数量就是all/32 然后细说这个值 这有一个叫core的东西&#xff0c;就是相当于th…

嵌入式学习-PWM输出比较

简介 PWM技术 输出比较框图介绍 定时器部分 比较器控制部分 输出控制部分 相关寄存器

(5.4–5.10)投融资周报|共38笔公开投融资事件,基础设施领跑,游戏融资活跃

5月4日至5月10日期间&#xff0c;加密市场共发生38笔投融资事件&#xff0c;其中基础设施18笔、游戏5 笔、其他4 笔、DeFi 3笔、Depin 3 笔、CeFi 2笔、NFT2笔、 RWA1笔。 本周千万美金以上融资有5笔&#xff1a; 加密货币交易公司Arbelos完成了一轮2800 万美元的种子轮融资&…

智慧园区EasyCVR视频智能管理方案:构建高效安全园区新视界

一、背景分析 园区作为城市的基本单元&#xff0c;是最重要的人口和产业聚集区。根据行业市场调研&#xff0c;90%以上城市居民工作与生活在园区进行&#xff0c;80%以上的GDP和90%以上的创新在园区内产生&#xff0c;可以说“城市&#xff0c;除了马路都是园区”。 园区形态…

C++ static_cast学习

static_cast可实现&#xff0c; 1 基本类型之间的转换 2 void指针转换为任意基本类型的指针 3 用于有继承关系的子类与父类之间的指针或引用的转换 用于基本类型转化时&#xff0c;会损失精度类似于C语言的强制转化&#xff1b; 下面先看一下void指针的转换&#xff1b; …

镜像抑制和镜像衰减有什么不同

在很多无线产品接收机手册中&#xff0c;我们会看到两个参数&#xff0c;一个是镜像抑制&#xff08;Image Rejection&#xff09;&#xff0c;另一个是镜像衰减&#xff08;Image Attention&#xff09;&#xff0c;但这两者究竟有什么不同&#xff0c;一直比较疑惑&#xff0…

三路输出小功率开关电源【MATLAB/simulink】

拟选用一种DC-DC变换器拓扑使用1700 V SiC MOSFET或IGBT设计三相功率系 统的高频开关直流辅助电源&#xff0c;它可用于太阳能逆变器、工业开关电源、电动汽车充电器、 电机驱动装置等领域。&#xff08;建议采用单端反激式电路拓扑&#xff0c;开关频率为80kHz) 电路基本参数&…

【Unity学习笔记】第十七 Quaternion 中 LookRotation、Lerp、Slerp、RotateTowards等方法辨析与验证

转载请注明出处: https://blog.csdn.net/weixin_44013533/article/details/138909256 作者&#xff1a;CSDN|Ringleader| 目录 Quaternion API 速览FromToRotation在Transform中的应用LookRotation 中upwards取Vector3.up和 transform.up的区别旋转时如何保持Y轴不变&#xff…

战网国际服怎么下载 暴雪战网一键下载安装图文教程

战网国际版&#xff0c;或称为Battle.net全球版&#xff0c;是暴雪娱乐构建的一项跨越国界的综合游戏交流平台&#xff0c;它无视地理限制&#xff0c;旨在服务全球每一个角落的游戏爱好者。不同于地区专属版本&#xff0c;国际版为玩家开启了一扇无门槛的大门&#xff0c;让每…

org.springframework.jdbc.BadSqlGrammarException

Cause: java.sql.SQLSyntaxErrorException: Table ‘web.emp’ doesn’t exist 产生原因&#xff1a;web表找不到&#xff0c;所以可能数据库配置错误 spring.datasource.urljdbc:mysql://localhost:3306/web02 更改完成后运行成功

免费利器:会议之眼一键生成论文功能火爆上线 助你快速起航

会议之眼 快讯 亲爱的会议之眼粉丝们&#xff0c;你们是否曾经为了写论文而彻夜苦思冥想&#xff1f;是否曾经为了找资料而焦头烂额&#xff1f; 今天小编带来了一个令人兴奋的消息&#xff0c;那就是会议之眼网页端平台的全新功能——“一键生成论文”已经重磅上线啦&#x…

【计算机毕业设计】springboot房地产销售管理系统的设计与实现

相比于以前的传统手工管理方式&#xff0c;智能化的管理方式可以大幅降低房地产公司的运营人员成本&#xff0c;实现了房地产销售的 标准化、制度化、程序化的管理&#xff0c;有效地防止了房地产销售的随意管理&#xff0c;提高了信息的处理速度和精确度&#xff0c;能够及时、…

STM32-09-IWDG

文章目录 STM32 IWDG1. IWDG2. IWDG框图3. IWDG寄存器4. IWDG寄存器操作步骤5. IWDG溢出时间计算6. IWDG配置步骤7. 代码实现 STM32 IWDG 1. IWDG IWDG Independent watchdog&#xff0c;即独立看门狗&#xff0c;本质上是一个定时器&#xff0c;这个定时器有一个输出端&#…

mmdetection训练(1)voc格式的数据集(自制)

mmdetection训练&#xff08;1&#xff09;voc格式的数据集&#xff08;自制&#xff09; 提前准备一、voc数据集二、修改配置代码进行训练&#xff08;敲黑板&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff09;1.数据集相关内容修改2.自定义配置文件构…

云曦实验室期中考核题

Web_SINGIN 解题&#xff1a; 点击打开环境&#xff0c;得 查看源代码&#xff0c;得 点开下面的超链接&#xff0c;得 看到一串base64编码&#xff0c;解码得flag 简简单单的文件上传 解题&#xff1a; 点击打开环境&#xff0c;得 可以看出这是一道文件上传的题目&#x…

【if条件、for循环、数据框连接、表达矩阵画箱线图】

编程能力&#xff0c;就是解决问题的能力&#xff0c;也是变优秀的能力 From 生物技能树 R语言基础第七节 文章目录 1.长脚本管理方式if(F){....}分成多个脚本&#xff0c;每个脚本最后保存Rdata&#xff0c;下一个脚本开头清空再加载 2.实战项目的组织方式方法&#xff08;一&…

圆上点云随机生成(人工制作模拟数据)

1、背景介绍 实际上,很多地物外表形状满足一定的几何形状结构,如圆形是作为常见一类。那么获取该类目标的点云数据便是位于一个圆上的点云数据。如下图所示为两簇典型的点云,其中一种为理想型,点均位于一个圆上,另外一簇则是近似位于一个圆上,这种更加符合真实情况。有时…