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://xiahunao.cn/news/2815865.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Qt程序设计-柱状温度计自定义控件实例

Qt程序设计-柱状温度计自定义控件实例 本文讲解Qt柱状温度计自定义控件实例。 效果演示 创建温度计类 #ifndef THERMOMETER_H #define THERMOMETER_H#include <QWidget> #include <QPainter> #include <QDebug> #include <QTimer> #include <QPr…

前端架构: 脚手架之包管理工具的案例对比及workspace特性的基本使用

Npm WorkSpace 特性 1 &#xff09;使用或不使用包管理工具的对比 vue-cli 这个脚手架使用 Lerna 管理&#xff0c;它的项目显得非常清晰在 vue-cli 中包含很多 package 点开进去&#xff0c;每一个包都有package.json它里面有很多项目&#xff0c;再没有 Lerna 之前去维护和管…

python+mysql咖啡店推荐系统django+vue

(1).研究的基本内容 系统的角色分为&#xff1a; 1.管理员 2.会员 3.非会员 角色不同&#xff0c;权限也不相同 技术栈 后端&#xff1a;python 前端&#xff1a;vue.jselementui 框架&#xff1a;django/flask Python版本&#xff1a;python3.7 数据库&#xff1a;mysql5.7…

TADHE车灯专用修复UV胶--汽车灯罩修复领域之光

车灯修复UV胶是一种专门用于修复车灯破损、裂痕、掉角等问题的特殊胶水。它具有以下功能&#xff1a; 快速固化&#xff1a;通过紫外光照射&#xff0c;车灯修复UV胶可以在短时间内迅速固化&#xff0c;通常只需5-15秒。这种快速固化的特性使得修复过程更加高效&#xff0c;节…

【GB28181】wvp-GB28181-pro修改分屏监控为16画面(前端)

引言 作为一个非前端开发人员,自己摸索起来比较费劲,也浪费了很多时间 由于实际开发中,可能预览的画面多于8个,而wvp目前只支持8画面 本文快速帮助开发者修改分屏监控为多画面。例如16画面,20画面等 文章目录 一、 预期效果展示16分割画面20分割画面二、 源码修改-前端修改…

Chapter 8 - 19. Congestion Management in TCP Storage Networks

Queue Depth Monitoring and Microburst Detection Queue depth monitoring and microburst detection capture the events that may cause congestion at a lower granularity but are unnoticed by other means due to long polling intervals. 队列深度监控和微爆检测可捕捉…

Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程

文章目录 1、问题一2、HTTP 请求流转过程概述图3、详细过程分析4、容器技术底座5、问题二6、详细过程分析(补充) 1、问题一 当外部发送一个HTTP/HTTPS 请求到Kubernetes 集群时&#xff0c;它是如何达到 Pod 中的 container 的呢&#xff1f; 2、HTTP 请求流转过程概述图 3、…

违背祖训,微软骚操作强制用户更新至 Win 11 23H2

话说&#xff0c;大伙儿有让 Windows 操作系统一直保持最新版习惯吗&#xff1f; 根据以往惯例&#xff0c;Windows 系统更新是个比较玄学的存在&#xff0c;谁也不能保证随手更新后会不会出现什么奇葩 Bug。 因此对于不少同学来说&#xff0c;Windows 更新到一个稳定版本后&a…

openGauss学习笔记-230 openGauss性能调优-系统调优-配置并行查询功能

文章目录 openGauss学习笔记-230 openGauss性能调优-系统调优-配置并行查询功能230.1 适用场景与限制230.2 资源对SMP性能的影响230.3 其他因素对SMP性能的影响230.4 配置步骤 openGauss学习笔记-230 openGauss性能调优-系统调优-配置并行查询功能 openGauss的SMP并行技术是一…

Linux磁盘如何分区?

首先需要先给虚拟机添加磁盘 sblk #查看磁盘设备 得到以下内容&#xff1a; NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT sda 8:0 0 20G 0 disk ├─sda1 8:1 0 1G 0 part /boot └─sda2 8:2 0 19G 0 pa…

Groovy - 大数据共享搜索配置

数据共享搜索列中配置了搜索列&#xff0c;相应的数据共享接口中也需要支持根据配置的字段搜索&#xff0c;配置实体时&#xff0c;支持搜索的入参code必须是searchKeys&#xff0c;且接口应该是需要支持分页&#xff08;入参必须是 current、pageSize&#xff09;的。current …

springboot+vue学生信息管理系统学籍 成绩 选课 奖惩,奖学金缴费idea maven mysql

技术栈 ide工具&#xff1a;IDEA 或者eclipse 编程语言: java 数据库: mysql5.7 框架&#xff1a;ssmspringboot都有 前端&#xff1a;vue.jsElementUI 详细技术&#xff1a;springbootSSMvueMYSQLMAVEN 数据库工具&#xff1a;Navicat/SQLyog都可以学生信息管理系统主要实现角…

NLP - 共现矩阵、Glove、评估词向量、词义

Word2vec算法优化 J(θ): 损失函数 问题&#xff1a;进行每个梯度更新时&#xff0c;都必须遍历整个语料库&#xff0c;需要等待很长的时间&#xff0c;优化将非常缓慢。 解决&#xff1a;不用梯度下降法&#xff0c;用随机梯度下降法 &#xff08;SGD&#xff09;。 减少噪音&…

MongoDB基础学习

文章目录 一、数据库1. 概念介绍2. 数据库分类 二、MongoDB入门1. 简介2. MongoDB的基本操作3. MongoDB文档间的关系4. Sort和投影 一、数据库 1. 概念介绍 数据库是按照数据结构来组织、存储和管理数据的仓库。我们的程序在内存中运行&#xff0c;一旦程序运行结束或者计算机…

【力扣 - 杨辉三角】

题目描述 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 示例 1: 输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] 示例 2: 输入: numRows 1 输出: [[1]] 提示: 1 < numRows < 30 方法一&#xff1a;数学 思路…

2024年2月19日-2月25日周报

文章目录 1. 本周计划2. 完成情况2.1 DCGANS网络架构2.2 SRGAN网络架构 3. 总结及收获4.下周计划 1. 本周计划 学习网络架构DCGANS和SRGAN 2. 完成情况 2.1 DCGANS网络架构 模型的核心&#xff1a;&#xff08;论文链接&#xff09; 取消池化层&#xff0c;使用带步长(str…

Leetcode—82. 删除排序链表中的重复元素 II【中等】

2024每日刷题&#xff08;117&#xff09; Leetcode—82. 删除排序链表中的重复元素 II 实现代码 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val…

计算机设计大赛 深度学习手势识别 - yolo python opencv cnn 机器视觉

文章目录 0 前言1 课题背景2 卷积神经网络2.1卷积层2.2 池化层2.3 激活函数2.4 全连接层2.5 使用tensorflow中keras模块实现卷积神经网络 3 YOLOV53.1 网络架构图3.2 输入端3.3 基准网络3.4 Neck网络3.5 Head输出层 4 数据集准备4.1 数据标注简介4.2 数据保存 5 模型训练5.1 修…

fork创建子进程及僵尸进程的产生及规避

本篇文章的学习与总结来源于 https://www.bilibili.com/cheese/play/ep182659?csourcecommon_hp_history_null&t3&spm_id_from333.1007.top_right_bar_window_history.content.click 通常使用fork()函数产生新的子进程&#xff0c;需要包含两个头文件<sys/types.h…

修改Qt生成iOS应用的原生底层,编译QtBase下的ios子模块

1.下载Qt源码 2.找到ios.pro子工程 3.使用QtCreaor12打开ios.pro工程 4.出现工程下只有一个.pro文件解决 复制修改好的toolchain.prf文件进行替换. 修改方法: