Skiplist跳表详解及其模拟实现
文章目录
- 跳表
- 1.跳表的概念
- 2.Skiplist在插入时采用随机层数的方法是如何保证效率的呢?
- 3.跳表的模拟实现
- 4.跳表VS平衡搜索树和哈希表
跳表
1.跳表的概念
跳表是基于有序链表扩展实现的。对于一个普通的有序链表,我们查找数据的时间复杂度是O(N)。而跳表的出现,让当前结点可能有能力跳过多个结点,以此来减少不必要的查找与比较,从而提高效率,跳表的时间复杂度是O(logN)。
跳表的发展过程:
- 假设我们让每相邻的两个结点升高一层,增加一个指针,让该指针指向下下个节点,如下图b中所示。这样所有新增加的指针又形成了一个新的链表(不同层级的),它包含的结点数目是原来的一般。因此在进行数据查找的时候,我们不需要与链表重的每个结点进行比较了,需要比较的结点数目变为了原来的一半(大约)。
- 我们可以在2层链表的基础上进行进一步的扩展,让每相邻的两个结点(都是2层链表的结点)升高一层,增加一个指针,从而产生三层链表,如下图c中所示。这样搜索效率进一步提高了。
跳表的这个提高链表层级的思路非常类似于二分查找,这使得它的查找时间复杂度可以降低到O(logN),因为每提高一层,搜索的效率就会提高一些。
但是这个结构在插入或删除数据的时候是存在很大缺陷的,插入或删除数据后,会打乱相邻两层链表上结点个数的严格的比例关系(2:1关系),因此如果要进行调整的话,也必须要把后面的所有结点都重新调整,这样会使时间复杂度退化为O(N)。
- 为了避免上述的问题,Skiplist在设计的时候不再要求严格的按照比例关系了。它要求在插入一个结点的时候,随机出一个层数。这样每次插入和删除的时候不需要再考虑其它结点的层数了。下图就是使用随机层数后的插入过程。
2.Skiplist在插入时采用随机层数的方法是如何保证效率的呢?
在此之前,我们先来了解一下Skiplist是如何实现随机层数的方法吧!
实现随机层数的方法:
在Redis的Skiplist的实现中,参数maxLevel和p的取值如下:
maxLevel = 32
p = 0.25
maxLevel表示最大层数,p表示每多一层的概率。
设当前层数level=1
产生层数为1的概率为(1 - p)
, 产生大于1层的概率为p
。
产生层数为2的概率为p * (1 - p)
,产生大于2层的概率为p^2
。
产生层数为3的概率为p^2 * (1 - p)
,产生大于3层的概率为p^3
。
产生层数为4的概率为p^3 * (1 - p)
,产生大于4层的概率为p^4
。
…
产生层数为n的概率为p^(n-1) * (1 - p)
,产生大于n层的概率为p^n
。
一个结点的平均层数(即,每个结点包含的平均指针数目),计算如下:
- 当p = 0.25时,每个结点的平均层数为1.33层。(每个结点包含的平均指针个数为1.33)
- 当p = 0.5时, 每个节点的平均层数为2层。 (每个节点包含的平均指针个数为2)
3.跳表的模拟实现
struct SkiplistNode{int val;vector<SkiplistNode*> nextV;SkiplistNode(int num, int level):val(num),nextV(level, nullptr){}
};class Skiplist {public:Skiplist() :head(new SkiplistNode(-1, 1)) //头结点处我们初始化它是1层的(其实也可以直接拉到32层maxLevel,不过性能略有损耗){srand(time(nullptr)); //生成随机数种子}bool search(int target) {SkiplistNode* cur = head;int level = cur->nextV.size() - 1; //层数的下标//当level小于0时退出循环while(level >= 0){//1. 当前结点的下一个为nullptr 或 下一个的值 > target, 此时向下走if(cur->nextV[level] == nullptr || cur->nextV[level]->val > target){--level;}//2. 当前结点的下一个不为nullptr && 下一个的值 < target, 此时向右走else if(cur->nextV[level]->val < target){cur = cur->nextV[level];}//3. 下一个的值 == targetelse{return true;}}return false;}//找prevV, add和erase均需要用vector<SkiplistNode*> findPrevVector(int target){SkiplistNode* cur = head;int level = cur->nextV.size() - 1; //下面使用的都是下标, 所以这里作-1处理vector<SkiplistNode*> prevV(level + 1, head); //这里的prevV数组中, "i下标处存储的是第i+1层"的前一个节点//这里prevV默认初始化必须为head, 因为SkipList为空时, 插入的前一个节点一定是headwhile(level >= 0){//1. 当下一个为nullptr 或 下一个的值 >= target, 此时向下走。(这里考虑允许数据冗余了)if(cur->nextV[level] == nullptr || cur->nextV[level]->val >= target){prevV[level] = cur; //prevV[level]存的是cur!!!--level;}else{//2. 当下一个不为nullptr && 下一个的值 < targetcur = cur->nextV[level];}}return prevV;}void add(int num) {int level = randomLevel(); //随机生成层数if(level > head->nextV.size()){ //判断是否需要更新头结点的层数head->nextV.resize(level, nullptr);}vector<SkiplistNode*> prevV = findPrevVector(num);SkiplistNode* newNode = new SkiplistNode(num, level);//更新连接关系for(size_t i = 0; i < level; ++i){newNode->nextV[i] = prevV[i]->nextV[i]; //prevV[i]: i下标就是第i+1层的前一个结点prevV[i]->nextV[i] = newNode;}}bool erase(int num) {vector<SkiplistNode*> prevV = findPrevVector(num);//先判断num值是否存在. 判断prevV[0]的下一个是否为nullptr, 以及下一个值是否为num//这里必须使用0下标, 其它下标的层数可能不存在if(prevV[0]->nextV[0] == nullptr || prevV[0]->nextV[0]->val != num){return false;}else{//删除, 并更新连接关系SkiplistNode* delNode = prevV[0]->nextV[0];for(size_t i = 0; i < delNode->nextV.size(); ++i){prevV[i]->nextV[i] = delNode->nextV[i];}delete delNode;//判断是否需要更新头结点nextV的大小(无关紧要)size_t i = head->nextV.size() - 1;for(; i >= 0; --i){if(head->nextV[i] != nullptr)break;}head->nextV.resize(i + 1);return true;}}int randomLevel(){int level = 1;//rand()的概率是[0, RAND_MAX], 我们这里限定rand() < RAND_MAX * p, 这个概率正好为pwhile(rand() <= RAND_MAX * p && level < maxLevel){++level;}return level;}private:SkiplistNode* head;size_t maxLevel = 32;double p = 0.25;
};
4.跳表VS平衡搜索树和哈希表
跳表本质是一种搜索结构,它根平衡搜索树和哈希表是同一领域的,它同时支持key或key/value的搜索模型。
- Skiplist相比平衡搜索树(AVL、红黑树),遍历数据时都可以做到有序输出,时间复杂度也差不多。
Skiplist的优势:
a. Skiplist的实现更简单,容易控制; 平衡搜索树的增删查改都较为复杂。
b. Skiplist的额外空间消耗更低,Skiplist在p=0.25时,每个结点包含的指针数目为1.33; 而平衡搜索树每个结点都是一个三叉链(存储3个指针),并且还需要额外存储平衡因子/颜色属性。 - Skiplist相比哈希表就没什么优势了。哈希表的时间复杂度为O(1), 要比Skiplist快一些,但是Skiplist的空间消耗要少一些。
Skiplist的优势:
a. 遍历数据有序。
b. Skiplist空间消耗少一些,哈希表存在表空间的消耗以及每个下标处挂的链表的指针消耗。
c. 哈希表扩容时有性能损耗。
d. 哈希表在极端场景下(哈希冲突高),效率会大幅度下降,此时需要使用红黑树来补救。
Skiplist跳表详解及其模拟实现相关推荐
- Java版skiplist跳表详解
skiplist简介 skiplist 是 一个概率型数据结构,查找.删除.插入的时间复杂度都是O(logN). skiplist是由多层有序的链表组成的,来加快查找速度. 其中第0层包含了所有元素, ...
- SkipList跳表详解
友情提示:下文在跳表插入数据时,会讲述如何动态维护索引,实现比较简单,逻辑比较绕,不要放弃,加油!!!如果一遍看不懂没关系,可以选择暂时性的跳过,毕竟这块偏向于源码.但是读者必须知道跳表的查找.插入. ...
- 二叉树,平衡二叉树,B-Tree,B+Tree,跳表详解
二叉树,平衡二叉树,B-Tree,B+Tree,跳表详解 1.二叉查找树(BST) 1.1 二叉查找树概念 1.2 二叉查找树特点 2. 平衡二叉树(AVL) 2.1 平衡二叉树概念 2.2 平衡二叉 ...
- SkipList(跳表)
SkipList(跳表) 文章目录 SkipList(跳表) 参考 前言 跳表的原理 跳表的插入和删除 插入操作 删除操作 跳表的时间空间复杂度分析 时间复杂度 空间复杂度 调表的基本操作 插入数据 ...
- 判断数组中某个元素除自身外是否和其他数据不同_算法工程师要懂的3种算法数据结构:线性表详解...
算法思想有很多,业界公认的常用算法思想有8种,分别是枚举.递推.递归.分治.贪心.试探法.动态迭代和模拟.当然8种只是一个大概的划分,是一个"仁者见仁.智者见智"的问题. 其实这些 ...
- 线性表详解(静态链表、单链表、双向链表、循环链表)
目录 申明 1. 线性表的定义 2. 线性表的抽象数据类型 3. 线性表的顺序存储结构 3. 1 顺序存储定义 3. 2 顺序存储方式 3. 3 数据长度与线性表长度区别 3. 4 地址计算方法 4. ...
- Activity 跳转详解
Activity 跳转详解 你好! 我是 Graydalf ,有可能也叫 Gdalf ~ 今天被朋友问到如何设置一个广播来启动一个应用并显示数据,于是将自己了解到的记录下来,有什么较为 DEMO 的地 ...
- skiplist 跳表(1)
最近学习中遇到一种新的数据结构,很实用,搬过来学习. 原文地址:skiplist 跳表 为什么选择跳表 目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等. ...
- 创建emp表 oracle,Oracle中创建和管理表详解
Oracle中创建和管理表详解 更新时间:2013年08月01日 15:44:16 作者: 以下是对Oracle中的创建和管理表进行了详细的分析介绍,需要的朋友可以过来参考下 SQL> /* ...
最新文章
- 阿里巴巴为什么不建议直接使用Async注解?
- Solaris和Linux的比较、区别、异同云云。。。
- jQuery原理系列-css选择器实现
- LEMP构建高性能WEB服务器(第三版)
- 又一游戏大盗(完美世界、跑跑等)SysInfo1.dll
- 介绍一种Web上打印技术
- Win7电脑创建本地连接网络的操作方法
- Java开发 - 异常 - 使用throws
- android控制软键盘显示与隐藏
- HDU 4273 Rescue(三维凸包重心)
- 据所有独立的c文件生成相应执行文件通用Makefile
- 计算机桌面颜色如何设置标准,电脑桌面背景字体颜色设置小技巧
- 调用百度图像识别api处理网络图片(文字识别)
- STM32开发项目:ADS1115的驱动与使用
- 基于龙芯CPU中标麒麟操作系统的国产半实物仿真系统ETestDEV
- 【Linux】Shell脚本:while read line无法读取最后一行???
- 【饭谈】面试官:速斩此子,切不可引狼入室
- 【C++·峰顶计划】引用操作及底层原理深析
- python并发测试脚本语言_python并发测试脚本
- matlab里面的vpa函数,matlab用vpa函数之后怎么科学计数法
热门文章
- CSDN日报180521——《如何选择值得深入学习的技术方向》
- ChatGPT作者John Schulman:通往TruthGPT之路
- kali linux安装upupoo_Kali Linux 下载、引导、安装
- UE5 tiles 材质缩放平铺
- linux错误代码255,kubelet 服务启动失败, 错误代码 255
- 技术太多学不过来?教你如何越学越带劲
- C盘全面清理教程,彻底清理所有垃圾
- plt.subplot()函数解析(最清晰的解释)
- arduino电子时钟 简易版
- 网络安全学习第4篇-使用特征码和MD5对勒索病毒进行专杀,并对加密文件进行解密