高阶数据结构跳表

"想象为翼,起飞~"


跳表简介?

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

跳表由来        

        skiplist是由美国计算机科学家William Pugh于1989年发明,skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。我们知道在对一个有序链表进行查找,它的时间复杂度为O(N)。

William Pugh开始了他的优化思路:

● 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图所示:

 这样新增的一层指针通过连接可以形成新的链表,它包含了整个链表节点的一半,由此需要在这一层进行比较、筛除的个数也就降低了一半。

        以此类推,继续增加一层指针,新链表的节点数下降,查找的效率自然而然也就提高了。按照上述每增加一层,节点数就少一半,其查找的过程类似于二分查找,使得查找的时间复杂度可以降低到O(LogN)。

        当然上述查找的前提是一个有序的链表。无论你是对其中的链表新增节点,还是删除节点,都可能打乱原有维持的指针连接,从而导致跳表失效。。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也 包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。

● 随机层数: 为了避免这种情况,skiplist的设计不再严格要求对应比例关系,而是,插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数。

 

skiplist如何保证效率?

        那么skiplist在引入随机层数后,如何保证其查找效率呢?首先,这个随机层数会有一个限制,这里把它叫做maxlevel,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下图:

        我们最终可以得到这样一个数学式,用于计算一个节点的平均层数:

有了这个公式,我们可以很容易计算出:

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

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

        至于跳表的平均时间复杂度为O(logN),这个推导的过程较为复杂,愚钝的我也就不在此摆弄文墨,下面的两篇中英文章可以给你提供你要的答案:
铁蕾大佬的博客:http://zhangtielei.com/posts/blog-redis-skiplist.html.

William_Pugh大佬的论文: http://ftp.cs.umd.edu/pub/skipLists/skiplists.pdf.


跳表实现:

        leetcode上有一道实现跳表的题,你可以在这上面完成跳表的测试: https://leetcode.cn/problems/design-skiplist/
        当然讲了这么多,还是没具体说说到底跳表是如何进行查找的,所以我们要实现的第一个函数接口就是跳表元素查找:

skipList初始化:

// 跳表不仅仅是要存储数据 _data
// 还需要有next指针,当然这些next指针也不止一个
// 这取决于 当前节点的层数
typedef struct SkiplistNode
{int _val;                       // 节点值vector<SkiplistNode*> _nextV;   // 节点连接的其他表项SkiplistNode(int val, int level):_val(val), _nextV(level, nullptr){}
}Node;class Skiplist {
public:Skiplist() {// 初始化 _headList// 默认给一层_head = new SkiplistNode(-1, 1);}
private:Node* _head;                // 头节点double _prate = 0.25;       // 新增层概率int _MaxLevel;              // 最大层数
};

 

Search:

    bool search(int target) {// 1.从头节点查Node* cur = _head;// 记录的层数// 0~n-1的下标int level = _head->_nextV.size() - 1;while (level >= 0){// target 大于 下一个节点的val cur向右移动if (cur->_nextV[level] && target > cur->_nextV[level]->_val){cur = cur->_nextV[level];}                           // 因为支持数据冗余 所以如果出现一样的就把新节点插在它后面即可else if(cur->_nextV[level]==nullptr || target < cur->_nextV[level]->_val){// target 小于 下一个节点的val --level || next节点为空--level;}else{// 找到了return true;}}return false;}

 

Add:

    vector<Node*> FindPath(int num){// 从头节点查起Node* cur = _head;int level = _head->_nextV.size()-1;// 开和level一样的大小vector<Node*> prevV(level+1,_head);while (level >= 0){if (cur->_nextV[level] && num > cur->_nextV[level]->_val){// 只管移动cur = cur->_nextV[level];} // 因为支持数据冗余 所以如果出现一样的就把新节点插在它后面即可else if (cur->_nextV[level] == nullptr || num <= cur->_nextV[level]->_val){// 记录该层num的前一个节点prevV[level] = cur;// 向下更新--level;}}return prevV;}int RandomLevel(){int level = 1;while (rand() <= _prate * RAND_MAX && level < _MaxLevel){++level;}return level;}void add(int num){// 前驱节点vector<Node*> prevV = FindPath(num);// 创建节点int n = RandomLevel();Node* newnode = new Node(num, n);// 可能创建节点层数 > _headif (n > _head->_nextV.size()){// 进行扩容_head->_nextV.resize(n,nullptr);// prevV也许跟着扩容// 这里的新增前驱节点为什么初始化为 _head?// 新增节点一定是连接在 prevV里的节点之后的prevV.resize(n, _head);}// 前后连接节点for (int i = 0;i < n;++i){// 可以理解为:newnode->next = prev->next->nextnewnode->_nextV[i] = prevV[i]->_nextV[i];prevV[i]->_nextV[i] = newnode; // 连接回来}}

        这里的randomLevel()是以一种巧妙的方式完成的:

         通过p可以控制最终值产生范围的概率。

        不过,C++有专门的随机数生成的库,比这个rand功能更加强大,所以我们可以将那个RandomLevel()改成这样:

    int RandomLevel(){// 随机数种子static static std::default_random_engine generator(std::chrono::system_clock::now().time_since_epoch().count());// 生成随机数范围static std::uniform_real_distribution<double> distribution(0.0,1.0);size_t level = 1;while (distribution(generator) <= _prate && level < _MaxLevel){++level;}return level;}

Erase:

    bool erase(int num){vector<Node*> prevV = FindPath(num);// 这里可能找不到if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num) return false;// 我们根据prevV的最底层节点 就可以找到del节点Node* del = prevV[0]->_nextV[0];// 根据该节点的层数 更新prevV 和 nextV// 进行连接for (int i = 0;i < del->_nextV.size();++i){prevV[i]->_nextV[i] = del->_nextV[i];}// 删除节点// 这里记录levelint level = del->_nextV.size();delete del;// 如果删除的节点是 最高层呢? 并且是唯一呢?// 这种优化可以不做 但你也可以做// 就是重新定义_head的高度// 向下遍历 只要遇到不为空的最高 就breakint i = _head->_nextV.size() - 1; while (i >= 0){if (_head->_nextV[i] == nullptr){--i;}else{break;}}_head->_nextV.resize(i + 1);return true;}

 

        最后我们可以通过leetcode提供的测试用例,来测试测试咱们写的跳表。 

跳表vs平衡搜索树和哈希表的对比

        最后一个话题:

        skiplist相比平衡搜索树(AVL树和红黑树)对比都可以做到遍历数据有序,时间复杂度也差不多。不过skiplist与平衡搜索树的最大优势在于:

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

● skiplist的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子/颜色等消耗。可是skiplist可以通过p来调整每个节点的指针个数,那是个可接受的数量。

        skiplist相比哈希表而言,在查找上就没有那么大的优势了。

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

        相反skiplist在这些方面胜过哈希表:
● 遍历数据有序

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

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


本篇到此结束,感谢你的阅读。

祝你好运,向阳而生~

 

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

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

相关文章

Matplotlib数据可视化(五)

目录 1.绘制折线图 2.绘制散点图 3.绘制直方图 4.绘制饼图 5.绘制箱线图 1.绘制折线图 import matplotlib.pyplot as plt import numpy as np %matplotlib inline x np.arange(9) y np.sin(x) z np.cos(x) # marker数据点样式&#xff0c;linewidth线宽&#xff0c;li…

Fegin异步情况丢失上下文问题

在微服务的开发中&#xff0c;我们经常需要服务之间的调用&#xff0c;并且为了提高效率使用异步的方式进行服务之间的调用&#xff0c;在这种异步的调用情况下会有一个严重的问题&#xff0c;丢失上文下 通过以上图片可以看出异步丢失上下文的原因是不在同一个线程&#xff0c…

基于React实现日历组件详细教程

前言 日历组件是常见的日期时间相关的组件&#xff0c;围绕日历组件设计师做出过各种尝试&#xff0c;展示的形式也是五花八门。但是对于前端开发者来讲&#xff0c;主要我们能够掌握核心思路&#xff0c;不管多么奇葩的设计我们都能够把它做出来。 本文将详细分析如何渲染一…

【Python原创毕设|课设】基于Python Flask的上海美食信息与可视化宣传网站项目-文末附下载方式以及往届优秀论文,原创项目其他均为抄袭

基于Python Flask的上海美食信息与可视化宣传网站&#xff08;获取方式访问文末官网&#xff09; 一、项目简介二、开发环境三、项目技术四、功能结构五、运行截图六、功能实现七、数据库设计八、源码获取 一、项目简介 随着大数据和人工智能技术的迅速发展&#xff0c;我们设…

解决Oracle中XML插入数据时的空格问题

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

2.含电热联合系统的微电网运行优化

含电热联合系统的微电网运行优化 MATLAB代码&#xff1a;含电热联合系统的微电网运行优化 关键词&#xff1a;微网 电热联合系统 优化调度 参考文档&#xff1a;《含电热联合系统的微电网运行优化》完全复现 仿真平台&#xff1a;MATLAB yalmipcplex [火]主要内容&#xf…

k8s 常用命令(三)

1、查看版本信息&#xff1a;kubectl version [rootmaster ~]# kubectl version [rootmaster ~]# kubectl version Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.3", GitCommit:"ca643a4d1f7bfe34773c74f7952…

JAVA开发环境接口swagger-ui使用总结

一、前言 swagger-ui是java开发中生产api说明文档的插件&#xff0c;这是后端工程师和前端工程师联调接口的桥梁。生成的文档就减少了很多没必要的沟通提高开发和测试效率。 二、 swagger-ui的使用 1、引入maven依赖 <dependency><groupId>io.springfox</grou…

cpolar+JuiceSSH实现手机端远程连接Linux服务器

文章目录 1. Linux安装cpolar2. 创建公网SSH连接地址3. JuiceSSH公网远程连接4. 固定连接SSH公网地址5. SSH固定地址连接测试 处于内网的虚拟机如何被外网访问呢?如何手机就能访问虚拟机呢? cpolarJuiceSSH 实现手机端远程连接Linux虚拟机(内网穿透,手机端连接Linux虚拟机) …

keepalived+lvs(DR)(四十六)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、作用 二、调度器配置 三、web节点配置 一、作用 使用keepalived解决lvs的单点故障 高可用集群 二、调度器配置 安装keepalived yum install -y k…

评测PlayStation Portal:价格、设计、连接选项等

PlayStation Portal发出PlayStation手持设备返回的信号。这款新设备最初在2023年3月的PlayStation Showcase上被宣布为Project Q&#xff0c;它将允许你通过强大的Wi-Fi信号在任何地方播放最好的PS5游戏。 虽然PlayStation Portal可能不是PlayStation Portable和PlayStation G…

leetcode几个数组题

数组理论基础 数组是存放在连续内存空间上的相同类型数据的集合 因为数组的在内存空间的地址是连续的&#xff0c;所以我们在删除或者增添元素的时候&#xff0c;就难免要移动其他元素的地址 二分查找 移除元素 有序数组的平方 209.长度最小的子数组

jenkins 日志输出显示时间戳的方式

网上很多方式比较片面&#xff0c;最新版插件直接使用即可无需更多操作。 使用方式如下&#xff1a; 1.安装插件 Timestamper 2.更新全局设置 系统设置-找到 Timestamper 勾选 Enabled for all Pipeline builds 也可修改时间戳格式。 帮助信息中显示 When checked, timesta…

马原——5.两大总特征(辩证法)

两大总特征是解释了世界是怎样存在的。 三大规律是对两大总特征的进一步细化 对立统一规律解释了世界是怎样联系的&#xff0c;为什么发展 量变质变规律解释了怎样发展 否定之否定规律那里发展 五对基本范畴解释了联系和发展环节上的逻辑 客观性&#xff1a;不以人的意志为转…

使用easyExcel导入导出Date类型的转换问题

起因&#xff1a;在业务需求上需要将Excel表中的日期导入&#xff0c;存储到数据库中&#xff0c;但是entity中的日期类型使用Date来接收&#xff0c;这样导致时间精确到秒。这时&#xff0c;即使使用DateTimeFormat("yyyy-MM-dd")也无法成功转换&#xff0c;会报如下…

SpringBoot案例-配置文件-@ConfigurationProperties

问题分析 在往期的配置参数的文章中&#xff0c;对于阿里云OSS的参数时设置在yml配置文件中&#xff0c;然后使用Value&#xff08;”${}“&#xff09;对参数进行赋值&#xff0c;具体如下&#xff1a; 此种方法比较繁琐 问题解决 使用注解 Data 为变量自动生成get/set方…

2023国赛数学建模思路 - 案例:异常检测

文章目录 赛题思路一、简介 -- 关于异常检测异常检测监督学习 二、异常检测算法2. 箱线图分析3. 基于距离/密度4. 基于划分思想 建模资料 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 一、简介 – 关于异常…

MyBatis进阶:告别SQL注入!MyBatis分页与特殊字符的正确使用方式

目录 引言 一、使用正确的方式实现分页 1.1.什么是分页 1.2.MyBatis中的分页实现方式 1.3.避免SQL注入的技巧 二、特殊字符的正确使用方式 2.1.什么是特殊字符 2.2.特殊字符在SQL查询中的作用 2.3.如何避免特殊字符引起的问题 2.3.1.使用CDATA区段 2.3.2.使用实体引…

怎么做出老板看得懂的财务数据分析报表?

财务数据分析报表的主要作用就是为决策提供必不可少的数据信息&#xff0c;让老板以及管理层在充分了解企业现金流情况、债务能力、还债能力、进账情况等财务信息后&#xff0c;更科学地做出运营管理决策。因此&#xff0c;财务数据分析报表必须做得直观易懂&#xff0c;毕竟不…

Qt与电脑管家3

1.ui页面设计技巧 最外面的widget&#xff1a; 上下左右的margin都置相同的值 这里有4个widget&#xff0c;做好一个后&#xff0c;后面3个可以直接复制.ui文件&#xff0c;然后进行微调即可。 2.现阶段实现的效果&#xff1a; 3.程序结构&#xff1a; btn1--->btn btn1---…