skiplist(高阶数据结构)

目录

一、概念

二、实现

三、对比


一、概念

skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》

skiplist本质上是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。skiplist是一个list,是在有序链表的基础上发展起来的。若是一个有序的链表,查找数据的时间复杂度是 O(N)

William Pugh开始的优化思路

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

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

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

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

skiplist的效率如何保证?

skiplist插入一个节点时随机出一个层数,听起来如此随意,如何保证搜索时的效率呢?

一般跳表会设计最大层数maxLevel的限制,其次会设置一个多增加一层的概率p,伪代码如下:

在Redis的skiplist实现中,这两个参数的取值为:maxLevel = 32p = 1/4

  • 结点层数至少为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

跳表的平均时间复杂度为O(logN),推导过程较为复杂,需要一定数学功底,有兴趣可以参考以下大佬文章中的讲解

Redis内部数据结构详解(6)——skiplist - 铁蕾的个人博客 Redis内部数据结构详解(6)——skiplist - 铁蕾的个人博客 - 作者:张铁蕾icon-default.png?t=N7T8http://zhangtielei.com/posts/blog-redis-skiplist.html

二、实现

https://leetcode.cn/problems/design-skiplist/description/

结点设计

struct SkiplistNode
{SkiplistNode(int value, int level):_value(value) ,_nextVector(level, nullptr) {}int _value;vector<SkiplistNode*> _nextVector;
};

Skiplist成员变量和构造函数

class Skiplist
{typedef SkiplistNode Node;
public:Skiplist(){srand(time(0)); //设置随机数种子,后续随机生成结点层数时使用_head = new SkiplistNode(-1, 1);//头结点初始层数为1}private:Node* _head;size_t maxLevel = 32; //最大层数限制double _p = 0.25; //多增加一层的概率
};

search函数                      

  • 记录当前所在结点以及所在结点的层数
  • 只有level >=0 时查找才有效,否则返回false
  • 若当前结点的值 > 目标值则向右走
  • 若当前结点的值 < 目标值 或者 同一层下一个结点为空(下标--)  ,则向下走
bool search(int target)
{Node* current = _head;int levelIndex = _head->_nextVector.size() - 1;while (levelIndex >= 0){//目标值比下一个结点的值大if (current->_nextVector[levelIndex] != nullptr && current->_nextVector[levelIndex]->_value < target)current = current->_nextVector[levelIndex]; //向右走//下一个结点是空(尾)//目标值比下一个结点要小else if (current->_nextVector[levelIndex] == nullptr || current->_nextVector[levelIndex]->_value > target)--levelIndex; //向下走elsereturn true; //找到了}return false;
}

FindPrevNode函数

设计该函数的目的:

  • add函数添加新结点要找到新结点每一层的前一个结点进行连接 
  • erase函数删除结点要找到该结点每一层前一个结点 与 每一层的后一个结点进行连接
  • 使代码简洁,设计了FindPrevNode函数,实现代码的复用
vector<Node*> FindPrevNode(int number)
{Node* current = _head;int levelIndex = _head->_nextVector.size() - 1;//待插入结点或待删除结点 的每一层的前一个结点的指针vector<Node*> prevVector(levelIndex + 1, _head);while (levelIndex >= 0){if (current->_nextVector[levelIndex] != nullptr && current->_nextVector[levelIndex]->_value < number)current = current->_nextVector[levelIndex];else if (current->_nextVector[levelIndex] == nullptr || current->_nextVector[levelIndex]->_value >= number){prevVector[levelIndex] = current;--levelIndex;}}return prevVector;
}

该函数基本与search函数相同,需要注意的是:当current->_nextVector[levelIndex]->_value >= number时,记录current结点。比search多一个等于,因为最低层的指针也需要修改链接

add函数

  • 获取要添加结点的前一个Node的集合
  • 随机获取层数,构建新结点并初始化
  • 若随机获取的层数超过当前最大的层数,那就升高一下_head的层数
  • 利用前一个Node集合 prevVector 和 当前结点的每一层建立连接关系
void add(int number)
{//获取要添加数据的前一个Node的集合vector<Node*> prevVector = FindPrevNode(number);//随机获取层数,构建新结点并初始化int level = RandomLevel();Node* newNode = new Node(number, level);//若随机获取的层数超过当前最大的层数,那就升高一下_head的层数if (level > _head->_nextVector.size()) {_head->_nextVector.resize(level, nullptr);prevVector.resize(level, _head);}//链接前后结点for (int i = 0; i < level; ++i) {newNode->_nextVector[i] = prevVector[i]->_nextVector[i];prevVector[i]->_nextVector[i] = newNode;}
}

为什么不一开始就将_head头结点的层数设为最高呢?

一开始设为最高,后序查找有很多是无用的,所以不直接将_head设为最高,且利用一个变量记录最高层,当新插入数据的层数 > 最高层时才增加层数

erase函数

  • 获取要删除结点的前一个Node的集合prevVector
  • 若prevVector[0]->_nextVector[0] == nullptr || prevVector[0]->_nextVector[0]->_value != number 即未找到该数据,返回false
  • 否则记录要删除的Node ,去除前后连接关系,然后delete释放资源
  • 若删除的是最高层节点,重新调整头结点层数,下次查找时就不会从无用的最高层开始查找 (这个过程做不做都行,提升不太大)
bool erase(int number)
{//获取要删除结点的前一个Node的集合vector<Node*> prevVector = FindPrevNode(number);if (prevVector[0]->_nextVector[0] == nullptr || prevVector[0]->_nextVector[0]->_value != number)return false;else{Node* deleteNode = prevVector[0]->_nextVector[0];// deleteNode结点每一层的前后指针链接起来for (int i = 0; i < deleteNode->_nextVector.size(); ++i)prevVector[i]->_nextVector[i] = deleteNode->_nextVector[i];delete deleteNode;//若删除的是最高层节点,重新调整头结点层数int headLevel = _head->_nextVector.size() - 1;while (headLevel >= 0){if (_head->_nextVector[headLevel] == nullptr)--headLevel;else break;}_head->_nextVector.resize(headLevel + 1);}return true;
}

获取随机数

方法一:C语言

int RandomLevel()
{size_t level = 1;// rand() ->[0, RAND_MAX]之间,将[0,RAND_MAX]看作为[0,1]while (rand() <= RAND_MAX * _p && level < _maxLevel)++level;return level;
}

方法二:C++

std::uniform_real_distribution<double> distribution(0.0, 1.0) ,随机生成0.0  -  1.0的数,生成的数是均匀分布的

int RandomLevelCPP()
{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) <= _p && level < _maxLevel)++level;return level;
}

三、对比

skiplist与红黑树、AVL树对比

  • skiplist和平衡搜索树(AVL树和红黑树)都可以做到遍历数据有序,时间复杂度也差不多
  • 但skiplist实现简单,容易控制。平衡树增删查改遍历都更复杂.
  • 并且skiplist的额外空间消耗更低。平衡树结点存储每个值有三叉链,平衡因子/颜色等消耗。skiplist中 p=1/2 时,每个结点所包含的平均指针数目为2;skiplist中 p=1/4 时,每个结点所包含的平均指针数目为1.33

skiplist与哈希表对比

skiplist与哈希表对比,就没有那么大的优势了。哈希表平均时间复杂度是 O(1),比skiplist快,但是哈希表空间消耗略多一点

  • 哈希表扩容有性能损耗
  • 哈希表在极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力

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

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

相关文章

AI:145-智能监控系统下的行人安全预警与法律合规分析

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带关键代码,详细讲解供大家学习,希望…

2024年阿里云2核4G配置服务器测评_ECS和轻量性能测评

阿里云2核4G服务器多少钱一年&#xff1f;2核4G服务器1个月费用多少&#xff1f;2核4G服务器30元3个月、85元一年&#xff0c;轻量应用服务器2核4G4M带宽165元一年&#xff0c;企业用户2核4G5M带宽199元一年。本文阿里云服务器网整理的2核4G参加活动的主机是ECS经济型e实例和u1…

Vue3制作一个可拖拽的小箭头

效果图 可以抓住小箭头进行左右拖拽&#xff0c;不会做git图&#xff0c;所以只有静态效果QAQ 代码 <template><div class"tip"draggable"true"dragstart"start" //拖拽开始时drag"dragging" //拖拽种dragend "…

2024.2.27每日一题

之前是出去旅游了没发&#xff0c;现在开学了&#xff0c;继续每日一题&#xff0c;继续卷&#xff0c;一上来就是困难题&#x1f613;&#xff0c;直接cv大法。 LeetCode 统计树中的合法路径数目 2867. 统计树中的合法路径数目 - 力扣&#xff08;LeetCode&#xff09; 题目…

选择何种操作系统作为网站服务器

选择操作系统时&#xff0c;需考虑稳定性、安全性、成本、兼容性和技术支持等因素&#xff0c;常见选项有Windows Server和Linux发行版。 选择网站服务器的操作系统是一个关键的决策&#xff0c;因为它将影响到网站的性能、稳定性、安全性以及未来的扩展性&#xff0c;目前市场…

数据库之ACID

一、ACID **原子性&#xff08;Atomicity&#xff09;&#xff1a;**即事务是不可分割的最小工作单元&#xff0c;事务内的操作要么全做&#xff0c;要么全不做&#xff0c;不能只做一部分&#xff1b; 一致性&#xff08;Consistency&#xff09;&#xff1a;在事务执行前数据…

【大数据】Flink SQL 语法篇(八):集合、Order By、Limit、TopN

Flink SQL 语法篇&#xff08;八&#xff09;&#xff1a;集合、Order By、Limit、TopN 1.集合操作2.Order By、Limit 子句2.1 Order By 子句2.2 Limit 子句 3.TopN 子句 1.集合操作 集合操作支持 Batch / Streaming 任务。 UNION&#xff1a;将集合合并并且去重。UNION ALL&a…

DataGrip 2023:让数据库开发变得更简单、更高效 mac/win版

JetBrains DataGrip 2023是一款功能强大的数据库IDE&#xff0c;专为数据库开发和管理而设计。通过DataGrip&#xff0c;您可以连接到各种关系型数据库管理系统(RDBMS)&#xff0c;并使用其提供的一组工具来查询、管理、编辑和开发数据库。 DataGrip 2023 软件获取 DataGrip …

[unity]lua热更新——个人复习笔记【侵删/有不足之处欢迎斧正】

一、AssetBundle AB包是特定于平台的资产压缩包&#xff0c;类似于压缩文件 相对于RESOURCES下的资源&#xff0c;AB包更加灵活轻量化&#xff0c;用于减小包体大小和热更新 可以在unity2019环境中直接下载Asset Bundle Browser 可以在其中设置关联 AB包生成的文件 AB包文件…

【Linux】云服务器的Redis被黑

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;Linux ⛺️稳中求进&#xff0c;晒太阳 攻击发现&#xff1a; 这个异常情况是在腾讯云被入侵后&#xff0c;短信提醒发现的。并没有系统的学习过关于服务器安防相关的知识&#xff0c;遇到…

国产动漫|基于Springboot的国产动漫网站设计与实现(源码+数据库+文档)

国产动漫网站目录 目录 基于Springboot的国产动漫网站设计与实现 一、前言 二、系统功能设计 三、系统功能设计 1、用户信息管理 2、国漫先驱管理 3、国漫之最管理 4、公告信息管理 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题…

C语言中如何进行内存管理

主页&#xff1a;17_Kevin-CSDN博客 收录专栏&#xff1a;《C语言》 C语言是一种强大而灵活的编程语言&#xff0c;但与其他高级语言不同&#xff0c;它要求程序员自己负责内存的管理。正确的内存管理对于程序的性能和稳定性至关重要。 一、引言 C 语言是一门广泛使用的编程语…

VPX基于全国产飞腾FT-2000+/64核+复旦微FPGA的计算刀片

6U VPX计算板 产品简介 产品特点 飞腾计算平台&#xff0c;国产化率100% VPX-MPU6902是一款基于飞腾FT-2000/64核的计算刀片&#xff0c;主频2.2GHz&#xff0c;负责业务数据流的管控和调度。搭配自带独立显示芯片的飞腾X100芯片&#xff0c;可用于于各类终端及服务器类应用场…

spring boot整合cache使用memcached

之前讲了 spring boot 整合 cache 做 simple redis Ehcache 三种工具的缓存 上文 windows系统下载安装 memcached 我们装了memcached 但spring boot没有将它的整合纳入进来 那么 我们就要自己来处理客户端 java历史上 有过三种客户端 那么 我们用肯定是用最好的 Xmemcached …

vue2 + axios + mock.js封装过程,包含mock.js获取数据时报404状态的解决记录,带图文,超详细!!!

vue axios mock.js 以下是封装的过程&#xff0c;记录一下 1、首先先了解什么是mock.js的用途及特点 官网地址&#xff1a;Mock.js (mockjs.com) 作用&#xff1a;生成随机数据&#xff0c;拦截 Ajax 请求 优势&#xff1a; 2、了解axios的原理及使用 官网地址&#xff1a…

大模型(LLM)的token学习记录-I

文章目录 基本概念什么是token?如何理解token的长度&#xff1f;使用openai tokenizer 观察token的相关信息open ai的模型 token的特点token如何映射到数值&#xff1f;token级操作&#xff1a;精确地操作文本token 设计的局限性 tokenizationtoken 数量对LLM 的影响训练模型参…

研发日记,MatlabSimulink开箱报告(九)——Simulink Test模块

文章目录 前言 Simulink Test模块 静态测试 动态测试 逻辑测试 前言 见《开箱报告&#xff0c;Simulink Toolbox库模块使用指南&#xff08;四&#xff09;——S-Fuction模块》 见《开箱报告&#xff0c;Simulink Toolbox库模块使用指南&#xff08;五&#xff09;——S-F…

练习 2 Web [ACTF2020 新生赛]BackupFile 1

[ACTF2020 新生赛]BackupFile 1 Web常规题目 首先尝试查找常见的前端页面index.php之类的&#xff0c;没找到 题目有个“BackupFile”——备份文件 尝试用工具遍历查找相关的文件 御剑没扫出来&#xff0c;搜索搭建好dirsearch后&#xff0c;扫出来的index.php.bak 扫描工…

迟到的VNCTF2024逆向题WP

这次比赛因为有事外出&#xff0c;只做了前两题&#xff0c;最近有空才把另外3题也做出来&#xff0c;总体来说比以往的VNCTF逆向题目要难一些。当然也有可能是我水平退步了&#xff0c;就算有时间参加比赛&#xff0c;应该也做不完这5题。VN的小伙伴越来越厉害了&#xff0c;出…

猜猜心里数字(个人学习笔记黑马学习)

1.定义一个变量&#xff0c;数字类型&#xff0c;内容随意 2.基于input语句输入猜想的数字&#xff0c;通过if和多次elif的组合&#xff0c;判断猜想数字是否和心里数字一致 num5if int(input("请输入第一次猜想的数字&#xff1a;"))5:print("猜对了&#xff0…