数据结构:跳表讲解

跳表

    • 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,一经查实,立即删除!

相关文章

C++面试高频问题汇总( 一)

文章目录 1. 面向对象 struct和class有什么区别1.1 封装什么是封装 1.2 继承1.3 多态1.3.1 什么是多态&#xff1f;1.3.2 虚函数作用和意义1.3.3 什么是纯虚函数和抽象类 2. STL2.1 什么是STL 其创建目的是什么2.2 STL的基本概念以及组成2.3 请简单介绍STL的六大组件2.4 什么是…

Fluter学习3 - Dart 空安全

Dart 空安全&#xff1a; 空类型操作符 (?)空值合并操作符 (??)空值断言操作符 (!)延迟初始化 (late) 1、空类型操作符 (?) 当你想要根据一个表达式是否为 null 来执行某个操作时&#xff0c;你可以使用 (?)语法&#xff1a;expression1?.expression2如果 expression1…

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

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

字节飞书面试算法题

后端]计算没有重复字符的子字符串数量 题目描述 给定你一个只包含小写英文字母的字符串 s 。如果一个子字符串不包含任何字符至少出现两次&#xff08;换句话说&#xff0c;它不包含重复字符&#xff09;&#xff0c;则称其为 特殊 子字符串。你的任务是计算 特殊 子字符串的数…

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

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…

2023年06月CCF-GESP编程能力等级认证C++编程三级真题解析

一、单选题(每题 2 分,共 30 分) 第1题 高级语言编写的程序需要经过以下( )操作,可以生成在计算机上运行的可执行代码。 A. 编辑 B. 保存 C. 调试 D. 编译 答案:D 第2题 二进制数 11.01 在十进制下是( )。 A. 3.01 B. 3.05 C. 3.125 D. 3.25 答案:D 第3题 已…

APK签名生成工具​

APK签名生成工具​ 点击下载安装签名生成工具&#xff0c;将你的应用的线上包安装到手机上&#xff0c;输入应用正确包名即可获取到签名信息。​ 注意&#xff1a;debug 包和线上包生成的签名是不一样的&#xff0c;所以这个地方要用线上包生成。​ 目前也支持使用keytool命…

每日一题——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;第一次写的时候也没思考直…

【Spring Boot 使用Filter统一处理请求数据转换】

Spring Boot 使用Filter统一处理请求数据转换 背景知识Spring Boot Filter 使用场景 请求预处理技术实现1. 创建自定义Filter类2. 创建自定义请求包装器3. 注册Filter 背景知识 Spring Boot Filter 使用场景 身份验证和授权 场景描述: 在用户访问应用程序的敏感资源之前&…

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

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

Python面向对象:组合

组合与重用性 组合指的是&#xff0c;在一个类中以另外一个类的对象作为数据属性&#xff0c;称为类的组合 class Equip: # 武器装备类 def fire(self): print(release Fire skill) class Riven: # 英雄Riven的类&#xff0c;一个英雄需要有装备&#xff0c;因而需要组合E…

【微服务生态】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常…