高阶数据结构跳表

"想象为翼,起飞~"


跳表简介?

        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.rhkb.cn/news/104070.html

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

相关文章

12、Pinia 快速入门

1、什么是Pinia Pinia 是 Vue 的最新 状态管理工具 &#xff0c;是 Vuex 的 替代品 2、手动添加Pinia到Vue项目 在实际开发项目的时候,关于Pinia的配置,可以在项目创建时自动添加 现在我们初次学习&#xff0c;从零开始&#xff1a; 1.使用 Vite 创建一个空的 Vue3 项目 n…

流媒体服务器SRS的搭建及QT下RTMP推流客户端的编写

一、前言 目前市面上有很多开源的流媒体服务器解决方案&#xff0c;常见的有SRS、EasyDarwin、ZLMediaKit和Monibuca。这几种的对比如下&#xff1a; &#xff08;本图来源&#xff1a;https://www.ngui.cc/zz/1781086.html?actiononClick&#xff09; 二、SRS的介绍 SRS&am…

python操作elasticsearch

python操作elasticsearch_一个高效工作的家伙的博客-CSDN博客 待更新

jstat(JVM Statistics Monitoring Tool):虚拟机统计信息监视工具

jstat&#xff08;JVM Statistics Monitoring Tool&#xff09;&#xff1a;虚拟机统计信息监视工具 用于监视虚拟机各种运行状态信息的命令行工具。 它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据&#xff0c;在没有GUI图形界面、只提…

[Linux]进程状态

[Linux]进程状态 文章目录 [Linux]进程状态进程状态的概念阻塞状态挂起状态Linux下的进程状态孤儿进程 进程状态的概念 了解进程状态前&#xff0c;首先要知道一个正在运行的进程不是无时无刻都在CPU上进行运算的&#xff0c;而是在操作系统的管理下&#xff0c;和其他正在运行…

Keepalived+Lvs(dr)调度器主备配置小实验

目录 前言 一、实验拓扑图 二、配置LVS&#xff08;dr&#xff09;模式 三、配置调配器热备 四、测试 总结 前言 Keepalived和LVS&#xff08;Linux Virtual Server&#xff09;是两个常用的开源软件&#xff0c;通常结合使用以提供高可用性和负载均衡的解决方案。 Keepalive…

如何获取Ck

1. 下载via浏览器 https://viayoo.com/zh-cn/ 2.打开via浏览器, 登录美团外卖 美团网账号登录-手机美团官网 3.点击左上角的盾牌 然后点击这里 最后去我的网站粘贴就行

Matplotlib学习笔记

Matplotlib数据可视化库 jupyter notebook优势 画图优势&#xff0c;画图与数据展示同时进行。数据展示优势&#xff0c;不需要二次运行&#xff0c;结果数据会保留。 Matplotlib画图工具 专用于开发2D图表以渐进、交互式方式实现数据可视化 常规绘图方法 子图与标注 想要…

linux中模拟RTOS中事件集

linux中通常如何处理事件集 在Linux中&#xff0c;没有直接对应于实时操作系统&#xff08;RTOS&#xff09;中事件集&#xff08;Event Set&#xff09;的概念。实时操作系统通常提供了一种机制&#xff0c;允许任务或线程根据事件的发生状态进行等待和唤醒。这通常通过信号量…

opencv 进阶13-Fisherfaces 人脸识别-函数cv2.face.FisherFaceRecognizer_create()

Fisherfaces 人脸识别 PCA 方法是 EigenFaces 方法的核心&#xff0c;它找到了最大化数据总方差特征的线性组合。不可否认&#xff0c;EigenFaces 是一种非常有效的方法&#xff0c;但是它的缺点在于在操作过程中会损失许多特征信息。 因此&#xff0c;在一些情况下&#xff0c…

CTFshow——web入门——反序列化web254-web278 详细Writeup

前言 在做题之前先简要总结一下知识点 private变量会被序列化为&#xff1a;\x00类名\x00变量名 protected变量会被序列化为: \x00\*\x00变量名 public变量会被序列化为&#xff1a;变量名__sleep() &#xff1a;//在对象被序列化之前运行__wakeup() //将在反序列化之后立即…

基于web的成语接龙游戏java jsp趣味学习mysql源代码

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 基于web的成语接龙游戏 系统有1权限&#xff1a;管理…

Wlan——锐捷智分网络解决方案及其配置

目录 智分解决方案 一代智分解决方案 二代智分解决方案 三代智分解决方案 智分解决方案 技术原理 隧道建立 智分方案的配置 配置基础信息 配置微AP的无线信号 调整微AP的射频参数 宿舍场景特点&#xff1a;房间小&#xff0c;单个房间用户少&#xff0c;房间密集&am…

jvm-类加载子系统

1.内存结构概述 类加载子系统负责从文件系统或网络中加载class文件&#xff0c;class文件在文件开头有特定的文件标识 ClassLoader只负责class文件的加载&#xff0c;至于它是否运行&#xff0c;则由Execution Engine决定 加载的类信息存放于一块称为方法区的内存空间&#xff…

论文解读:Image-Adaptive YOLO for Object Detection in Adverse Weather Conditions

发布时间&#xff1a;2022.4.4 (2021发布&#xff0c;进过多次修订) 论文地址&#xff1a;https://arxiv.org/pdf/2112.08088.pdf 项目地址&#xff1a;https://github.com/wenyyu/Image-Adaptive-YOLO 虽然基于深度学习的目标检测方法在传统数据集上取得了很好的结果&#xf…

ResNet18云空间部署

1-6步骤可以在云空间运行&#xff0c;也可以在本地运行&#xff1b;步骤7 在云空间运行。 1.编译ONNX模型 本章以 resnet18.onnx 为例, 介绍如何编译迁移一个onnx模型至BM1684X TPU平台运行。 该模型来自onnx的官网: models/vision/classification/resnet/model/resnet18-v1…

综合能源系统(8)——综合能源系统支撑技术

综合能源系统关键技术与典型案例  何泽家&#xff0c;李德智主编 1、大数据技术 1.1、大数据技术概述 大数据是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合&#xff0c;是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高…

自动驾驶仿真:基于Carsim开发的加速度请求模型

文章目录 前言一、加速度输出变量问题澄清二、配置Carsim动力学模型三、配置Carsim驾驶员模型四、添加VS Command代码五、Run Control联合仿真六、加速度模型效果验证 前言 1、自动驾驶行业中&#xff0c;算法端对于纵向控制的功能预留接口基本都是加速度&#xff0c;我们需要…

【Unity小技巧】Unity探究自制对象池和官方内置对象池(ObjectPool)的使用

文章目录 前言不使用对象池使用官方内置对象池应用 自制对象池总结源码参考完结 前言 对象池&#xff08;Object Pool&#xff09;是一种软件设计模式&#xff0c;用于管理和重用已创建的对象。在对象池中&#xff0c;一组预先创建的对象被维护在一个池中&#xff0c;并在需要时…

树结构使用实例---实现数组和树结构的转换

文章目录 一、为什么要用树结构&#xff1f;二、使用步骤 1.引入相关json2.树结构的转换总结 一、为什么要用树结构&#xff1f; 本文将讲述一个实例&#xff0c;构造一棵树来实现数组和tree的转换&#xff0c;这在前端树结构中是经常遇到的 后端返回树结构方便管理&#xff…