数据结构——跳表

简单介绍跳表

  跳表(Skip List)是一种可以进行对数级别查找的数据结构,它通过在数据中构建多级索引来提高查询效率。跳表是一种基于链表的随机化数据结构,其本质是由多个链表组成,每个链表中的元素都是原始链表中的元素。

  我们知道链表的查找时间复杂度是O(N),如果这个链表数据是有序的,还是O(N),我们如何利用有序这一点,来进行优化呢?接下来就是我们的主角跳表登场。

  跳表在实际应用中有许多用途,例如作为Redis等数据库的有序数据结构实现,以及作为平衡树等数据结构的替代方案。与其他数据结构相比,跳表具有实现简单、空间复杂度低、查询效率高等优点。

  skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A
Probabilistic Alternative to Balanced Trees》。
William Pugh开始的优化思路:
1. 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图b所
示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由
于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概
只有原来的一半。
2. 以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一
个指针,从而产生第三层链表。如下图c,这样搜索效率就进一步提高了。
3. skiplist正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方
式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似
二分查找,使得查找的时间复杂度可以降低到O(log n)。但是这个结构在插入删除数据的时
候有很大的问题,插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格
的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也
包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。
理想状态的跳表大概长这样:
但是一旦我们进行了插入和删除操作,就会很难维护这个2:1的关系。因此
4. skiplist的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是
插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数,
这样就好处理多了。细节过程入下图:

 

注意重点,在这种处理下,我们插入的结点的层数是随机的!如此一来,插入删除操作就简化了很多。

skiplist的效率 

  首先要分析,这个随机层数是怎么来的。一般跳表会设置一个最大的层数限制maxLevel。其次会设计一个概率p。这个p就是指 最开始从第一层开始,每多一层的概率为p。

  最大层数限制很好理解,这个p就是每次插入的时候,由它来决定这个结点有多少层。每多一层其实就是多一个指针。

在Redis中,这两个参数的取值为

p = 1/4
maxLevel = 32

 

再简单分析这个p就是:

节点层数至少为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)。
……

平均计算如下:

 

 skiplist的简单实现

  其实skiplist只要理解了思想,实现起来还是比较简单了,我们可以参考力扣上的一道题

1206. 设计跳表 - 力扣(LeetCode) 

 代码:

#pragma once#include <iostream>
#include <vector>
#include <time.h>
#include <random>
#include <chrono>using namespace std;struct SkiplistNode
{int _val;vector<SkiplistNode*> _nextV;  // 层数也就是指针,用数组存起来SkiplistNode(int val, int level):_val(val), _nextV(level, nullptr){}
};class Skiplist
{typedef SkiplistNode Node;
public:Skiplist(){srand(time(0));// 头结点的层数设置为1_head = new SkiplistNode(-1, 1);}bool search(int target){Node* cur = _head;int level = _head->_nextV.size() - 1;while (level >= 0){// 目标值比下一个结点的值要大的话,就往右走// 如果下一个结点是空(尾),或者目标值比下一个结点要小,就向下走if (cur->_nextV[level] && cur->_nextV[level]->_val < target){// 往右走cur = cur->_nextV[level];}else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val > target){// 往下走--level;}else{// 找到了return true;}}return false;}vector<Node*> FindPrevNode(int num){Node* cur = _head;int level = _head->_nextV.size() - 1;// 记录 被改动的位置的每一层的前一个结点指针vector<Node*> prevV(level + 1, _head);while (level >= 0){// 同理 比下一个结点大就往右走// 下一个结点是空,或者目标值比下一个结点要小,就往下走if (cur->_nextV[level] && cur->_nextV[level]->_val < num){cur = cur->_nextV[level]; // 向右走}else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val >= num) {// 注意 这里num等于也是要进来的// 更新level层的前一个prevV[level] = cur;--level; // 向下走}}return prevV;}void add(int num){vector<Node*> prevV = FindPrevNode(num);int n = RandomLevel();Node* newnode = new Node(num, n);// 注意:如果n超过了当前的最大层,那么就要相应的提高_head的层数if (n > _head->_nextV.size()){_head->_nextV.resize(n, nullptr);prevV.resize(n, _head);}// 链接前后结点for (size_t i = 0; i < n; ++i){// 注意顺序,先设置好目标结点的指针指向newnode->_nextV[i] = prevV[i]->_nextV[i];prevV[i]->_nextV[i] = newnode;}}bool erase(int num){vector<Node*> prevV = FindPrevNode(num);// 如果第一层的下一个不是val,则val不在表中if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num){return false;}else{Node* del = prevV[0]->_nextV[0];// 先将del结点的前后结点链接起来for (size_t i = 0; i < del->_nextV.size(); ++i){prevV[i]->_nextV[i] = del->_nextV[i];}delete del;// 如果删除的这个结点改变了跳表结点的当前最高层数,// 那么应该将头结点的层数降到第二高的层数int i = _head->_nextV.size() - 1;while (i >= 0){if (_head->_nextV[i] == nullptr)--i;elsebreak;}_head->_nextV.resize(i + 1);return true;}return false;}int RandomLevel(){size_t level = 1;// rand() 的取值范围在 [0,RAND_MAX] 之间// 这里就转换成了 如果区间在 [0,_p]就加一层while (rand() <= RAND_MAX * _p && level < _maxLevel){++level;}return level;}
private:Node* _head;size_t _maxLevel = 32;double _p = 0.25;
};void Test()
{Skiplist sl;//int a[] = { 5, 2, 3, 8, 9, 6, 5, 2, 3, 8, 9, 6, 5, 2, 3, 8, 9, 6 };int a[] = { 1, 2, 3, 4 };for (auto e : a){sl.add(e);}/*int x;cin >> x;sl.erase(x);*/sl.erase(1);//cout << sl.erase(0) << " " << sl.erase(1) << endl;
}

  跳表稍复杂一点的地方就是插入和删除了。因为我们还要需要找到目标结点的每一层前一个结点,将它们放入数组中,然后才能处理好目标结点的前后结点之间的关系。

  另外就是它的查找逻辑,设cur来遍历结点,那么cur的移动就有两种情况,一种是目标值大于cur下一个结点的值的话,cur就向右走;另一种就是小于,那么cur就向下走(--level)。再然后就是等于了。

跳表跟平衡搜索树及哈希表的对比

跟平衡搜索树 

  二者都可以做到遍历数据有序,并且时间复杂度都差不多。

  跳表跟平衡搜索树(AVL树和RB树)的优势就是:

1. 跳表实现简单,且容易控制。不像AVL树和RB树非常复杂,跳表这里我们删除操作都很容易就实现了。

2.跳表的空间消耗相对较低,不像平衡搜索树,不仅每个结点都有三叉链(指针),而且还要存平衡因子或者颜色。当跳表中的p = 1/2时,每个结点所包含的指针个数为2;p = 1/4时,每个结点所包含的指针个数为1.33。

因此,跟平衡搜索树比起来,还有是有优势的。

跟哈希表

跳表跟哈希表比起来,各有优缺点。

跳表的优点:

1.空间消耗还是略低哈希表。哈希表存在链表指针和表空间的消耗。

2.跳表遍历数据能有序。

3.哈希表扩容时有性能损耗。跳表就没有。

4.在极端场景下,哈希表哈希冲突高,效率下降的厉害,还需要红黑树来接力,增加了算法复杂度。

哈希表的优点:

时间复杂度是O(1),比跳表要快。

所以这样看来,跳表跟哈希表比起来,有些还是有优势的,但是没有跟平衡搜索树比起来那么大。

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

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

相关文章

图论 - Trie树(字符串统计、最大异或对)

文章目录 前言Part 1&#xff1a;Trie字符串统计1.题目描述输入格式输出格式数据范围输入样例输出样例 2.算法 Part 2&#xff1a;最大异或对1.题目描述输入格式输出格式数据范围输入样例输出样例 2.算法 前言 本篇博客将介绍Trie树的常见应用&#xff0c;包括&#xff1a;Trie…

电子电气架构——车载以太网协议栈

电子电气架构——车载以太网协议栈 我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 没有人关注你。也无需有人关注你。你必须承认自己的价值&#xff0c…

MySQL入门------数据库与SQL概述

目录 前言 一、数据库相关概念 二、数据模型 1.关系型数据库&#xff08;RDBMS&#xff09; 三、MySQL数据库 1.下载和安装 2.配置环境变量 四、SQL 1.SQL通用语法 2.SQL分类 前言 从本期开始&#xff0c;我们开始学习数据库的相关理论和实践知识&#xff0c;从入门…

jupyter 用pyecharts进行数据分析

一、jupyter和pyecharts下载和打开 因为我是用的pycharm&#xff0c;所以我直接在pycharm项目终端中下载pip install jupyter,pip install pyecharts 在你下载的项目路径中输入jupyter notebook 之后会进入页面 Jupyter 具体使用参考这个链接&#xff1a;Jupyter Notebook基本…

Pygame教程01:初识pygame游戏模块

Pygame是一个用于创建基本的2D游戏和图形应用程序。它提供了一套丰富的工具&#xff0c;让开发者能够轻松地创建游戏和其他图形应用程序。Pygame 支持许多功能&#xff0c;包括图像和声音处理、事件处理、碰撞检测、字体渲染等。 Pygame 是在 SDL&#xff08;Simple DirectMed…

常用设计模式详解

设计模式 1.UML图 统一建模语言是用来设计软件的可视化建模语言。定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。 1.1类图 1.1.1类的表示方式 在UML类图中&#xff0c;类使用包含类名、属性(field) 和方法(method) 且带有分割线…

Java基于springboot的厨艺交流平台的设计与实现代码

摘 要 使用旧方法对厨艺交流信息进行系统化管理已经不再让人们信赖了&#xff0c;把现在的网络信息技术运用在厨艺交流信息的管理上面可以解决许多信息管理上面的难题&#xff0c;比如处理数据时间很长&#xff0c;数据存在错误不能及时纠正等问题。 这次开发的厨艺交流平台功…

Redis的主从搭建

1.准备两台机器&#xff0c;安装好redis 2.修改从服务器的redis配置 slaveof <masterip> <masterport>两个参数 masterip 主的ip 主的端口号 masterport 3. 启动redis 1.先启动主机redis 2.再启用从机redis 主机redis日志打印 从机redis 日志打印

【python】1.python3.12.2和pycharm社区版的安装指南

欢迎来CILMY23的博客喔&#xff0c;本篇为【python】1.python3.12.2和pycharm社区版的安装指南&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 目录 一、python3.12.2的下载与安装 1.1下载 1.2安装 二、pycharm的安装 2.1下载安装 2…

Bootstrap的使用

目录 js的引入&#xff1a; 1.行内式 2.嵌入式 3.外链式 Bootstrap:的引入 注意事项&#xff1a; 条件注释语句&#xff1a; 栅格系统&#xff1a; 列嵌套&#xff1a; 列偏移&#xff1a; 列排序&#xff1a; 响应式工具&#xff1a; Bootstrap的字体图标的使用&a…

2024最新算法:河马优化算法(Hippopotamus optimization algorithm,HO)求解23个基准函数,提供MATLAB代码

一、河马优化算法 河马优化算法&#xff08;Hippopotamus optimization algorithm&#xff0c;HO&#xff09;由Amiri等人于2024年提出&#xff0c;该算法模拟了河马在河流或池塘中的位置更新、针对捕食者的防御策略以及规避方法。河马优化算法的灵感来自河马生活中观察到的三…

【测试工具】Fiddler

1.Fiddler简介 Fiddler是位于客户端和服务器端的HTTP代理&#xff0c;能够记录客户端和服务器之间的所有 HTTP请求&#xff0c;是web调试的利器。既然是代理&#xff0c;也就是说&#xff1a;客户端的所有请求都要先经过Fiddler&#xff0c;然后转发到相应的服务器&#xff0c…

day02-JavaScript-Vue

文章目录 1 JavaScript1.1 介绍 1.2 引入方式1.3 基础语法1.3.1 书写语法1.3.2 变量1.3.3 数据类型和运算符 1.4 函数1.4.1 第一种定义格式1.4.2 第二种定义格式 1.5 JavaScript对象1.5.1 基本对象1.5.1.1 Array对象语法格式特点属性和方法 1.5.1.2 String对象语法格式属性和方…

17.来自Sora的夺舍妄想——享元模式详解

OpenAI 的 Sora 模型面世之后&#xff0c;可以说人类抵御AI的最后阵地也沦陷了。 在此之前&#xff0c;人们面对AI交互式对话&#xff0c;AI制图&#xff0c;AI建模之类的奇迹时&#xff0c;还可以略微放肆的说&#xff1a;“的确很神奇&#xff0c;这毕竟还是比人类世界低了一…

2024年腾讯云部署幻兽帕鲁服务器,如何选择合适的服务器配置套餐畅玩游戏?

选择合适的服务器配置套餐以畅玩《幻兽帕鲁》游戏&#xff0c;首先需要考虑的是玩家数量和对服务器性能的需求。根据腾讯云提供的配置推荐&#xff0c;对于4到8人的玩家&#xff0c;推荐配置为4核16G12M&#xff1b;而10到20人的玩家则建议选择8核32G22M配置。这是因为《幻兽帕…

小程序页面指定区域局部滚动,做上拉和触底刷新

业务需求&#xff1a;在页面某个固定区域滑动 思路&#xff1a;滑动高度 页面高度 - 自定义导航高度&#xff08;不是自己自定义的导航可以省略&#xff09;- 按钮高度 - 单词数高度 实现 &#xff1a; 1.数据展示区内使用scroll-view&#xff0c;设置y轴滚动&#xff08;…

swoole

php是单线程。php是靠多进程来处理任务&#xff0c;任何后端语言都可以采用多进程处理方式。如我们常用的php-fpm进程管理器。线程与协程,大小的关系是进程>线程>协程,而我们所说的swoole让php实现了多线程,其实在这里来说,就是好比让php创建了多个进程,每个进程执行一条…

初阶数据结构:二叉树

目录 1. 树的相关概念1.1 简述&#xff1a;树1.2 树的概念补充 2. 二叉树2.1 二叉树的概念2.2 二叉树的性质2.3 二叉树的存储结构与堆2.3.1 存储结构2.3.2 堆的概念2.3.3 堆的实现2.3.3.1 堆的向上调整法2.3.3.2 堆的向下调整算法2.3.3.3 堆的实现 1. 树的相关概念 1.1 简述&a…

【C++ AVL树】

文章目录 AVL树AVL树的概念AVL树节点的定义AVL树的插入AVL树的旋转右单旋左单旋左右双旋右左双旋 代码实现 总结 AVL树 AVL树的概念 二叉搜索树在顺序有序或接近有序的情况下&#xff0c;而插入搜索树将退化为单叉树&#xff0c;此时查找的时间复杂度为O(n)&#xff0c;效率低…

鸿蒙Harmony应用开发—ArkTS声明式开发(通用属性:颜色渐变)

设置组件的颜色渐变效果。 说明&#xff1a; 从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 linearGradient linearGradient(value: { angle?: number | string; direction?: GradientDirection; colors: Array; repea…