【C++从0到王者】第五十二站:跳表

文章目录

  • 一、什么是跳表
  • 二、skiplist的效率
  • 三、skiplist的实现

一、什么是跳表

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

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

William Pugh开始的优化思路:

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

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

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

image-20240227123323106

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

image-20240227123351614

比如在下图的第三个跳表中,如果我们想要查找19的话是这样进行的

  1. 比9大,向右走,跳跃到9
  2. 比21小,向下走
  3. 比17大,向右走,跳跃到17
  4. 比21小,向下走
  5. 根19相等,找到了

image-20240227123857031

如果我们采用每个节点的高度是随机的,那么这样的话,每个节点插入和删除就跟其他节点没有关系了,都是独立的,不需要调整其他节点的层数了

image-20240227124030959

二、skiplist的效率

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

image-20240227124121898

在Redis的skiplist实现中,这两个参数的取值为:

p = 1/4
maxLevel = 32

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

  • 节点层数至少为1。而大于1的节点层数,满足一个概率分布。

  • 节点层数恰好等于1的概率为1-p。

  • 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。

  • 节点层数大于等于3的概率为p2,而节点层数恰好等于3的概率为p2*(1-p)。

  • 节点层数大于等于4的概率为p3,而节点层数恰好等于4的概率为p3*(1-p)。
    ……

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

image-20240227124314213

  • 现在很容易计算出:

  • 当p=1/2时,每个节点所包含的平均指针数目为2;

  • 当p=1/4时,每个节点所包含的平均指针数目为1.33。

  • 跳表的平均时间复杂度为O(logN)

三、skiplist的实现

这里我们使用这道题目来进行实现

跳表

我们的完整代码为

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <vector>
#include <time.h>
using namespace std;
struct SkiplistNode //跳表节点
{int _val;       //该节点所存储的值vector<SkiplistNode*> _nextV; //表明该节点所指向的下面的节点的指针。因为跳表会有多个指针,这个数量是不确定的,所以我们使用一个vectorSkiplistNode(int val, int level) //一个跳表节点被创建出来以后,需要它的值和该节点的层数,这是它最关键的两个信息:_val(val), _nextV(level, nullptr) //这里姑且先将新开的一个跳表节点的所有指针全部置空,后序在进行处理{}
};
class Skiplist {typedef SkiplistNode Node;
public:Skiplist() {srand(time(nullptr)); //因为跳表节点的层数是随机的,所以我们一定会用到rand函数,所以就要生成随机数种子,而它只需要调用一次,所以我们不妨直接在构造函数里面去调用//头节点,层数是1_head = new SkiplistNode(-1, 1); //当我们的跳表生成以后,我们让跳表姑且只有一个节点,并且这个节点不存储任何有效值,且其层数为1}//查找一个目标值是否在跳表中,如果存在,则返回truebool search(int target) {Node* cur = _head; //从头节点开始一直往下去遍历int level = _head->_nextV.size() - 1; //head的最高层数,其实也就是我们整个跳表的最高层数已经被确定了//因为寻找逻辑是向右和像下去跑的。如果向右去跑,一定是target太大了导致的,这时候一定会导致的是最终cur->_nextV[level]为nullptr。//此时跟据我们内部的逻辑也会向下走。最终level一定会降低到-1,此时就是没有找到了//如果原来的值太小,那么一定是一直往下跳,最终level也会降低到-1while (level >= 0)  {//cur的第level层所指向的那个结点的val小于目标结点//注意,这里cur的第level层可能指向空,但是右边可能还有结点,所以我们也需要让它向下移动if (cur->_nextV[level] && cur->_nextV[level]->_val < target){//直接跳到这个结点去,即向右跳cur = cur->_nextV[level];}//如果大于else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val > target){//向下跳level--;}else{return true;}}return false;}//这个函数的功能是,寻找指向num节点的所有指针。即前面的指向它的节点我们都会通过这个函数最终找到,返回一个vector,这个vector就是按照层去排好的vector<Node*> FindPrevNode(int num){//需要知道插入位置每一层的前一个结点指针。Node* cur = _head;int level = _head->_nextV.size() - 1; //先算出当前最大层数//我们要将每一层的前一个节点指针放入prevV中,注意level这个其实是下标,我们这里要是个数,所以要+1,并且它的初始时刻一定为_head。//prevV的数量为_head的层数的原因是,_head一定是当前跳表中层数最大的节点之一,即便后序num的比_head的的层数要高,我们后序可以通过resize去再次拔高_head//而初始时刻设置为_head的原因是,任何一个节点,如果它的层数//如果它和_head之间某一层没有相隔的节点,那么它此时的该层的上一个节点就是_head,而我们并不知道我们要找的num有几层(因为还没有定下来),所以我们可以直接将全部值设置为_head//然后如果它的prevV[level]不是_head了,那么直接覆盖即可。vector<Node*> prevV(level + 1, _head);//num存在的位置一定是要比cur的后面节点小于等于,但是又比cur节点处的位置大的值。while (level >= 0){//目标值比下一个节点值要大,向右走if (cur->_nextV[level] && cur->_nextV[level]->_val < num){//cur向右走cur = cur->_nextV[level];}//比num小于等于cur处,就可以更新它的前一个节点了,就是cur,然后我们这一层就找好了,去找下一层了。else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val >= num){//更新前一个结点    //如果等于nullptr,那么其实该处已经映射到头了,只要num是够高的,那么该节点就是指向num的。对于num小于等于,也是一样的道理。说明num就存在于该处//他的节点一定不会收到后面的影响了。所以只需要将前面所有节点的投影给拿出来即可prevV[level] = cur; level--;}}return prevV;}void add(int num) {//num将要插入位置的每一层的上一个节点指针数组vector<Node*> prevV = FindPrevNode(num);int n = RandomLevel(); //随机生成一个层数Node* newnode = new Node(num, n); //创建好新的跳表节点if (n > _head->_nextV.size())//如果新的层数已经超出了原有的层数,那么_head需要拔高,且prevV里面的数据也要拔高{_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) {//找到num对应的上一个节点指针数组vector<Node*> prevV = FindPrevNode(num);//最底层的下一个不是val,没有这个节点if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num){return false;}else{//要删除的节点就是最底层的节点指向的下一个节点Node* del = prevV[0]->_nextV[0];//del每一层的前后指针进行连接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; //寻找_head的高度}else{break;}}_head->_nextV.resize(i + 1); //降低_head的高度return true;}}//通过概率去控制层数的函数int RandomLevel(){size_t level = 1;while (rand() < RAND_MAX * _p && level < _maxLevel){++level;}return level;}//方便我们去观察跳表,去打印跳表void Print(){//int level = _head->_nextV.size();//for (int i = level - 1; i >= 0; i--)//{//    Node* cur = _head;//    while (cur)//    {//        printf("%d->", cur->_val);//        cur = cur->_nextV[i];//    }//    cout << endl;//}Node* cur = _head;while (cur){for (auto e : cur->_nextV){printf("%2d", cur->_val);}cout << endl;// 打印每个每个cur节点for (auto e : cur->_nextV){printf("%2s", "↓");}printf("\n");cur = cur->_nextV[0];}}
private:Node* _head; //跳表的第一个节点指针,即头节点,不存储有效数据size_t _maxLevel = 32; //最高的层数double _p = 0.5; //一层的概率
};int main()
{Skiplist sl;sl.Print();cout << "-------------------" << endl;int a[] = { 5,2,3,8,9,6 };for (auto e : a){sl.add(e);sl.Print();cout << "-------------------" << endl;}for (auto e : a){sl.erase(e);sl.Print();cout << "-------------------" << endl;}return 0;
}/*** Your Skiplist object will be instantiated and called as such:* Skiplist* obj = new Skiplist();* bool param_1 = obj->search(target);* obj->add(num);* bool param_3 = obj->erase(num);*/
//int main()
//{
//    Skiplist sl;
//    int max = 0;
//    for (size_t i = 0; i < 1000000000; i++)
//    {
//        int r = sl.RandomLevel();
//        if (max < r)
//        {
//            max = r;
//        }
//    }
//    cout << max << endl;
//    
//	return 0;
//}

在力扣上是可以通过测试用例的。

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

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

相关文章

V1.2基站设置IP及设置基站连接服务器

基站状态指示灯 基站正常连接上服务器&#xff0c;基站指示灯如下&#xff0c;第一个灯是电源指示灯常亮&#xff1b;第二个灯为运行指示灯&#xff0c;程序正常运行第二个灯一直闪烁&#xff1b;第三个灯为为网络指示灯&#xff0c;网络连接正常会常亮&#xff0c;网络不通或…

专注于 DeFi 的 EVM 兼容 L1项目Berachain加入Hack.Summit()2024区块链开发者大会的联合主办之一

在激动人心的区块链领域&#xff0c;我们很荣幸的宣布Berachain已成Hack.Summit()2024区块链开发者大会的联合主办方之一。这一盛会将汇聚全球顶尖的开发者、投资者和创新者&#xff0c;共同探讨区块链技术的未来发展。 作为比特币生态的领军项目&#xff0c;Berachain始终积极…

AI入门笔记(四)

深度学习是人工智能的一种实现方法。本文我将学习到的关于深度学习的代表卷积神经网络的数学结构分享给大家。 深度学习是重叠了很多层的隐藏层&#xff08;中间层&#xff09;的神经网络。我们以一个例题为例。 建立一个卷积神经网络&#xff0c;用来识别通过 66 像素的图像读…

LINE社群:为您的跨境出海业务带来更多流量

LINE 社群就是一个大型的公开聊天室&#xff0c;通过LINE社群不需要将对方添加为好友就可以聊天。它主要是以「兴趣」作为区分&#xff0c;所以商家可以在社群中找到不少潜在客户。尤其是面向台湾、日本、泰国这些地区的商家&#xff0c;LINE在这些地区的普及度很高&#xff0c…

云流化技术方案的优势

数字化的时代&#xff0c;许多新兴的技术都逐渐走进人们的视野&#xff0c;云流化作为一种新兴的技术在各个领域发挥着越来越重要的作用&#xff0c;也为我们带来了方便快捷的使用体验&#xff0c;尤其是在虚拟仿真和数字孪生领域&#xff0c;但是有的人可能听到这个词会比较陌…

RT-Thread软件+华大开发板 配置USART+MSH控制台

Step1: 打开IDE 并新建一个工程文件 step2: 在工程文件名右键 -> 点击串口->配置项 Step3: 点击 “硬件” -> “Enable UART” -> "Enable UART7" Step4: 点击“组件” -> “MSH: command shell” Step5: 保存整个工程&#xff0c; 并打开工…

使用 Docker 部署 Next Terminal 轻量级堡垒机

1&#xff09;Next Terminal 介绍 官网&#xff1a;https://next-terminal.typesafe.cn/ GitHub&#xff1a;https://github.com/dushixiang/next-terminal 想必经常玩服务器的都了解过 堡垒机&#xff0c;类似于跳板机&#xff0c;但与跳板机的侧重点不同。堡垒机的主要功能是…

java poi-tl 使用 模板 生成文档

1.引入依赖 <!--导出doc模板工具--><dependency><groupId>com.deepoove</groupId><artifactId>poi-tl</artifactId><version>1.10.6</version></dependency> 2.编码 /*** 导出doc文件* param id* return*/GetMapping(…

快乐生活,快乐分享,5款实用的小软件推荐

​ 分享是一种神奇的东西&#xff0c;它使快乐增大&#xff0c;它使悲伤减小&#xff0c;坚持分享一些好用的软件给大家&#xff0c;今天继续为大家带来五款好用的小软件。 1.数字笔记应用——OneNote ​ OneNote是微软推出的数字笔记应用&#xff0c;提供了一个集中的地方来…

泡菜生产加工厂污水处理需要哪些工艺设备

泡菜生产加工厂污水处理是一个重要的环境问题&#xff0c;需要采用一系列工艺设备来有效处理污水并降低对环境的影响。以下是一些常见的工艺设备&#xff0c;可以在泡菜生产加工厂中使用&#xff1a; 1. 沉淀池&#xff1a;沉淀池是最常用的工艺设备之一&#xff0c;其主要作用…

产品说明文件的二维码怎么做?扫码看文件在线生成技巧

现在很多企业会把产品说明书的文件做成二维码后&#xff0c;印刷到产品包装或者宣传展板上&#xff0c;让其他人通过扫码来查看说明&#xff0c;有效的提高了用户体验。那么文件二维码制作的方法和步骤可能有很多小伙伴都不太清楚&#xff0c;那么今天小编通过本篇文章来给大家…

城市基础信息管理系统 (VB版电子地图源码/公交车线路图/超市平面图)-143-(代码+程序说明)

转载地址http://www.3q2008.com/soft/search.asp?keyword143 请访问 以下地址,查看最新版本, 新增加支持 建筑物 距离测量, 鸟瞰, 地图放大缩小, VB完善地图扩充程序(城市街道基础信息管理系统 )-362-&#xff08;代码&#xff0b;论文&#xff09; 这套系统印象深刻 因为,写…

[云原生] K8s之ingress

1.Ingress的相关知识 1.1 Ingress的简介 service的作用体现在两个方面&#xff0c;对集群内部&#xff0c;它不断跟踪pod的变化&#xff0c;更新endpoint中对应pod的对象&#xff0c;提供了ip不断变化的pod的服务发现机制&#xff1b;对集群外部&#xff0c;他类似负载均衡器…

VideoDubber时长可控的视频配音方法

本次分享由中国人民大学、微软亚洲研究院联合投稿于AAAI 2023的一篇专门为视频配音任务定制的机器翻译的工作《VideoDubber: Machine Translation with Speech-Aware Length Control for Video Dubbing》。这个工作将电影或电视节目中的原始语音翻译成目标语言。 论文地址&…

软件测试 基础(2)

文章目录 1. 软件测试&软件开发生命周期2. 如何描述一个 BUG3. 如何定义 BUG 的级别4. BUG 的生命周期5. 如何进行第一次测试6. 测试的执行和 BUG 管理7. 产生争执怎么办&#xff08;处理人际关系&#xff09; 1. 软件测试&软件开发生命周期 软件测试的生命周期&#…

Matplotlib图形配置--自定义坐标刻度

文章目录 自定义坐标刻度编程要求代码解释 自定义坐标刻度 虽然matplotlib默认的坐标轴定位器与格式生成器可以满足大部分需求&#xff0c;但是并非对每一幅图都合适。 主次要刻度 学习前最好先对matplotlib图形的对象层级有深入了解。 matplotlib的figure对象是一个盛放图形…

Oracle集群ASM磁盘扩容

先通过lsblk来查看集群两端磁盘是否扩容成功 lsblk -a查看自己两边磁盘UUID是否相同(for i in 后面为磁盘sd后字母名) for i in x y z aa ab ac ad ae do echo "KERNEL\"sd*\", SUBSYSTEM\"block\", PROGRAM\"/lib/udev/scsi_id --whitelist…

Vue 3中的ref:响应式变量的强大工具

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

jeecgboot 开放页面权限,免登录访问

前端需要配置路由和添加白名单 1、配置路由 2、 在permission.js里&#xff0c;把刚才的路由添加到白名单 3、 后端需要把该页面涉及到的接口排除权限拦截 比如我这个页面涉及到两个接口&#xff1a; 那么就在后端的excludeUrls把这两个接口加进去。 前端后端都设置好了&…

AI皮肤测试的基本原理

人工智能测试皮肤的实现原理通常涉及计算机视觉和机器学习技术。以下是一般的实现步骤和原理&#xff0c;涉及数据收集、特征提取、模型训练和优化等多个步骤&#xff0c;利用这些步骤中的技术手段来实现对皮肤状况的识别和分类。北京木奇移动技术有限公司&#xff0c;专业的软…