DS高阶:跳表

news2024/12/23 17:27:18

一、skiplist

1.1 skiplist的概念

        skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key或者key/value的查找模型。skiplist是由William Pugh发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》 

        skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。如果是一个有序的链表,查找数据的时间复杂度是O(N)。


1.2 skiplist的优化思路分析

           假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图b所
示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半。(多层链表的启发思路)以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。如下图c,这样搜索效率就进一步提高了。

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

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

 1.3 随机出层数的含义

         插入节点时随机出一个层数究竟是什么意思呢???难道直接random任意数就可以了吗??

答:虽然是随机,但是也有规则的限制。这里首先要细节分析的是这个随机层数是怎么来的。一般跳表会设计一个最大层数maxLevel的限制,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下图:

 一个节点的平均层数(也即包含的平均指针数目),计算如下:

现在很容易计算出:
当p=1/2时,每个节点所包含的平均指针数目为2;
当p=1/4时,每个节点所包含的平均指针数目为1.33。

时间复杂度:logN

具体的分析可以看下面的文章:Redis内部数据结构详解(6)——skiplist
 

2、skiplist的模拟实现

力扣有一道设计跳表的题. - 力扣(LeetCode)设计跳表

基本的调表需要实现4个函数:构造函数、搜索、插入、删除。下面我们来一个个分析。

2.1 skiplist的基本结构

struct SkiplistNode
{
	int _val;//存储对应的值
	vector<SkiplistNode*> _nextV;//存放对应的next指针集合
	SkiplistNode(const int&val, size_t level = 1) //level表示需要开辟的层数 不传就是默认开满
		:_val(val)
	{
		_nextV.resize(level, nullptr);
	}
};




class Skiplist
{
	typedef  SkiplistNode Node;
public:


private:
	Node* _head;//虚拟头节点
    const size_t _maxLevel = 32; //用缺省参数去初始化
    const double _p = 0.25;//用缺省参数去初始化
};

 2.2 skiplist的默认构造

Skiplist()
{
	srand((unsigned int)time(nullptr));//为了方便后面的随机取层数,先弄一个随机种子
	_head = new Node(-1);//默认开一层,用默认构造初始化
}

        给虚拟头节点申请一块空间,一开始默认就开一层。为了能够方面后面利用rand函数随机取层数,所以在这个地方先用了一个时间种子

         我们默认开的是一层,因为在数据量小的时候其实我们可以根据插入的情况去调整_head的层数,如果是数据量特别大的话,也可以一次性就把他开到满

2.3 skiplist的搜索

bool search(int target) 
{
	//要不断往下走
	Node* cur = _head;
	int level = _head->_nextV.size() - 1;//从后往前去找
	while (level >= 0)
	{
	   //如果我比你大 我就跳过去->更新cur   
	   //如果我比你小或者你为空 我就往下走 --level
		if (cur->_nextV[level] == nullptr || target < cur->_nextV[level]->_val) --level;
		else if (target > cur->_nextV[level]->_val)  cur = cur->_nextV[level];
		else return true;
	}
	return false; //循环结束都没有找到,说明找不到。
}

我们要从高层一直找到底层,所以要从_nextV的后面开始找。

1、如果你为空,或者我比你小,那就得往下走 ->--level

2、如果我比你大,就可以直接跳到你的位置->更新cur=cur->_nextV[level]

3、如果找到了就返回true,如果循环结束了都找不到,那就返回false

2.4 找到prevV指针数组

为什么要单独去封装这个函数呢?

         因为不管是插入,还是删除,我们都需要去找前驱节点的集合,这样才能去改变连接关系,所以为了提高代码的复用性,封装这样的一个函数,去找到待插入位置或者是待删除位置的前驱节点集合。

vector<Node*> FindPrevNode(int num) //帮助我们找到前驱指针集合
{
  //最终我们要返回待插入位置或者是待删除位置的前驱指针集合  一开始的时候默认是head、
	Node* cur = _head;
	int level = _head->_nextV.size() - 1;
	vector<Node*> prevV(level+1, _head);
	while (level >= 0)
	{
		if (cur->_nextV[level] == nullptr || num < cur->_nextV[level]->_val)
		{
			//更新level的层的前一个节点 往下跳之前保存前驱节点
			prevV[level] = cur;
			--level;
		}
		else//(num >= cur->_nextV[level]->_val)  
			cur = cur->_nextV[level];
	}
	return prevV;
}

        当我们需要往后面跳之前,保存当前的cur进去prevV数组中,这样我们返回的数组就是待插入节点对应的前驱节点集合了!

2.5 随机层数的生成函数

      我们在插入节点之前,要随机生成一个层数,所以要先实现一个生成层数的函数

2.5.1 C语言rand( )版本

size_t RandomLevel() //C语言版本
{
	size_t level = 1;//初始的层数
	while (rand() <= RAND_MAX * _p && level < _maxLevel)  ++level; //RAND_MAX是随机数的最大值
	return level;
}

2.5.2 C++11随机数库

	size_t RandomLevel() //需要的时候去搜 C++11的随机数库即可  头文件chrono和random
	{
		//类似随机数种子,但是只用一次是最好的 所以设置成staic 这样就只会调用一次了
		static std::default_random_engine generator(std::chrono::system_clock::now().time_since_epoch().count());//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;
	}

         std::chrono::system_clock::now().time_since_epoch().count() 类似一个时间戳,相当于是随机种子,但是由于只需要初始化一次,所以我们将他变成static变量,这样就只要初始化一次即可!

        关于C++11的random库用法,还是比较复杂的,大家可以参考一些相关的文章。

2.6 skiplist的增加

void add(int num)  //插入节点
{
	vector<Node*> prevV = FindPrevNode(num); //右值引用
	size_t n = RandomLevel(); //表示需要开多少层
	//如果n超过了_head的最大层数,那么就要调整一下
	if (n > _head->_nextV.size())
	{
		_head->_nextV.resize(n, nullptr); 
		prevV.resize(n, _head);//不够的地方也要更新过去
	}
	Node* newnode = new Node(num, n);//申请对应的新节点  然后根据prevV数组去建立连接
	for (size_t i = 0; i < n; ++i) //连接前后节点,首先要先连后面的 再连前面的
	{
		newnode->_nextV[i] = prevV[i]->_nextV[i];
		prevV[i]->_nextV[i] = newnode;
	}
}

         一个很关键的地方就是,我们随机生成了一个层数后,有可能我们的_head的层数都没这个多,所以我们必须利用resize去初始化一下,否则会出现越界访问。 

         中间插入的逻辑就类似链表的指定位置插入,先让自己的后继指向前驱的后继,然后再让前驱指向自己,必须按照这个顺序,否则会丢失节点

2.7 skiplist的删除

bool erase(int num) 
{
	//首先 有可能没有这个数 所以要看看是不是真的没有
	vector<Node*> prevV = FindPrevNode(num);
	if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)  return false;
	//有的话,就要去删除然后重新连接
	Node* del = prevV[0]->_nextV[0];//我们需要删除的节点,但是在删除前要调整一下连接的关系
	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;
		else  break;
	}
	_head->_nextV.resize(i + 1);
	return true;
}

     有可能我们找不到这个数,这个时候就没什么可以删的了。

      在删除这个节点之前,我们要先记录这个节点,然后去改变被删除节点的连接关系,类似链表的指定位置删除。

      如果我们删除的恰好是最高层的节点,这个时候可以整体对头结点的层数降个高度,这样就提高了查找效率。

三、skiplist跟平衡搜索树和哈希表的对比

1. skiplist相比平衡搜索树(AVL树和红黑树)对比,都可以做到遍历数据有序,时间复杂度也差不多。但是skiplist在平衡树面前优势明显。

skiplist的优势是:

a、skiplist实现简单,容易控制。平衡树增删查改遍历都更复杂。

b、skiplist的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子/颜色等消耗skiplist中p=1/2时,每个节点所包含的平均指针数目为2;skiplist中p=1/4时,每个节点所包含的平均指针数目为1.33;


2. skiplist相比哈希表而言,就没有那么大的优势了:

哈希表的优势如下:

a、哈希表平均时间复杂度是O(1),比skiplist快。

b、哈希表空间消耗略多一点。

skiplist优势如下:

a、遍历数据有序

b、skiplist空间消耗略小一点,哈希表存在链接指针和表空间消耗。

c、哈希表扩容有性能损耗。

d、哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力。
 

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

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

相关文章

SOLIDWORKS科研版的优势

随着科技的不断进步&#xff0c;科研领域对于快捷、准确和可视化的需求也在不断增长。在这个背景下&#xff0c;SOLIDWORKS科研版应运而生&#xff0c;为科研人员提供了一款强大的工具&#xff0c;帮助他们解决复杂的问题&#xff0c;提高研究效率。 首先&#xff0c;SOLIDWOR…

贷款借钱平台 贷款源码 小额贷款系统 卡卡贷源码 小额贷款源码 贷款平台

贷款平台源码/卡卡贷源码/小贷源码/完美版 &#xff0c; 数据库替换application/database.php 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89268533 更多资源下载&#xff1a;关注我。

镜像抑制和镜像衰减有什么不同

在很多无线产品接收机手册中&#xff0c;我们会看到两个参数&#xff0c;一个是镜像抑制&#xff08;Image Rejection&#xff09;&#xff0c;另一个是镜像衰减&#xff08;Image Attention&#xff09;&#xff0c;但这两者究竟有什么不同&#xff0c;一直比较疑惑&#xff0…

Microsoft 安全Copilot:适时而生的得力工具

随着数字化转型的加速&#xff0c;网络安全威胁日益增多&#xff0c;Microsoft Copilot for Security的出现恰逢其时&#xff0c;它将帮助我们更好地应对这些挑战&#xff0c;保护我们的数据和系统免受攻击。 ✦什么是Microsoft 安全Copilot✦ 概述 Microsoft 安全 Copilot 是…

Docker 的 RHEL 操作系统镜像

CentOS 和 Fedora 都是和 RHEL 同源衍生版本的 Linux&#xff0c;因为 CentOS 不需要向 RHEL 支付企业级的版权和费用&#xff0c;但是又具有和 RHEL 相同的稳定性和资源库。 所以 CentOS 在服务器上被广泛的部署和使用&#xff0c;但是 CentOS 目前已经停止技术支持和结束了生…

Nerf基础学习记录

笔记 什么是nerf一些值得注意的理论点 什么是nerf Nerf 是可以理解成是一种隐式的3D表达方式。 隐式表达可以理解成是用一个条件或者方程来表示一个3D几何&#xff0c;没有实实在在的几何形状。相反&#xff0c;显式表达就诸如mesh, 点云&#xff0c;体素这类的&#xff0c;能…

【if条件、for循环、数据框连接、表达矩阵画箱线图】

编程能力&#xff0c;就是解决问题的能力&#xff0c;也是变优秀的能力 From 生物技能树 R语言基础第七节 文章目录 1.长脚本管理方式if(F){....}分成多个脚本&#xff0c;每个脚本最后保存Rdata&#xff0c;下一个脚本开头清空再加载 2.实战项目的组织方式方法&#xff08;一&…

CTFshow misc

第一题1 打开图片直接就是flag 第二题0 放入010发现文件头有png 更换后缀 获得flag 第三题1 下载之后发现是bpg后缀 用在线工具转换为png获得flag 第四题 0 把六个文件后缀都改为png即可获得flag

springboot005学生心理咨询评估系统

springboot005学生心理咨询评估系统 亲测完美运行带论文&#xff1a;获取源码&#xff0c;私信评论或者v:niliuapp 运行视频 包含的文件列表&#xff08;含论文&#xff09; 数据库脚本&#xff1a;db.sql其他文件&#xff1a;ppt.ppt论文&#xff1a;开题.doc论文&#xf…

二.使用PgAdmin连接Postgresql

二.使用PgAdmin连接Postgresql PostgreSQL是一种开源的对象关系型数据库管理系统(ORDBMS),它支持大部分SQL标准并提供了许多高级功能,例如事务、外键、视图、触发器等。PostgreSQL由PostgreSQL全球开发组维护和开发,它是一种高度可扩展的数据库系统,可以在各种操作系统…

Java | Leetcode Java题解之第91题解码方法

题目&#xff1a; 题解&#xff1a; class Solution {public int numDecodings(String s) {int n s.length();// a f[i-2], b f[i-1], cf[i]int a 0, b 1, c 0;for (int i 1; i < n; i) {c 0;if (s.charAt(i - 1) ! 0) {c b;}if (i > 1 && s.charAt(i …

49-Qt控件详解:ltemViewsltemWidgets

1.List View:清单视图 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QListView> //字符串列表模型 #include <QStringListModel> #include <QMessageBox> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEcl…

Flutter+Getx仿小米商城项目实战教程又新增了Flutter调用原生地图

FlutterGetx仿小米商城项目实战教程基于Flutter3.x录制&#xff0c;课程紧贴企业需求&#xff0c;目前已完结176讲。教程所讲内容支持Android、Ios、华为鸿蒙OS&#xff0c;教程更新于2024年4月09日新增 Flutter 调用百度地图、新增Flutter充电桩项目地图实战。支持2024年3月29…

RAW转换和图像编辑工具:Capture One 23 Pro (win/mac)中文专业版

Capture One 23是一款功能强大的桌面版照片编辑软件&#xff0c;由丹麦PHASE ONE飞思数码公司开发。 以下是该软件的一些主要特点&#xff1a; 强大的RAW处理功能&#xff1a;Capture One 23支持多种品牌的相机和镜头&#xff0c;提供了丰富的RAW处理工具&#xff0c;包括曝光、…

适合建站的香港服务器有哪些,企业和个人建站的

香港服务器适合外贸建站、个人和企业建站&#xff0c;尤其是中小企业官网非常适合放在香港服务器上&#xff0c;因为香港服务器在国内外的访问速度都很快&#xff0c;也就意味着全球客户都能访问到你的网站。 对于很多新手小白来说不知道怎么才能买到靠谱稳定的香港服务器&…

C语言 | Leetcode C语言题解之第91题解码方法

题目&#xff1a; 题解&#xff1a; int numDecodings(char* s) {int n strlen(s);// a f[i-2], b f[i-1], c f[i]int a 0, b 1, c;for (int i 1; i < n; i) {c 0;if (s[i - 1] ! 0) {c b;}if (i > 1 && s[i - 2] ! 0 && ((s[i - 2] - 0) * 10…

C++进阶:哈希(2)位图与布隆过滤器

目录 1. 位图&#xff08;bitset&#xff09;1.1 引子&#xff1a;海量整形数据的处理1.2 结构描述1.3 位图实现1.4 位图相关题目练习 2. 布隆过滤器&#xff08;BloomFilter&#xff09;2.1 引子&#xff1a;海量非int类型数据处理&#xff08;string&#xff09;2.2 结构描述…

【Linux:环境变量】

环境变量一般是指在操作系统中用来指定操作系统环境的一些参数 常见的环境变量&#xff1a; PATH 指定可执行程序的搜索路径 系统级的文件&#xff1a;/etc/bashrc 用户级文件&#xff1a;~/.bashrc ~/.bash_profile HOME 指定用户的主要工作目录&#xff08;当前用…

react18【系列实用教程】useReducer —— 升级版的 useState (2024最新版)

useReducer 可看做升级版的 useState &#xff0c;其强大之处在于&#xff0c;可以自定义复杂的响应式变量修改逻辑。 useReducer 语法 useReducer 是 hook 函数 第一个参数&#xff08;必要&#xff09;&#xff1a; 自定义的 reducer 函数&#xff08;详见下文介绍&#xff…

英伟达发布 VILA 视觉语言模型,实现多图像推理、增强型上下文学习,性能超越 LLaVA-1.5

前言 近年来&#xff0c;大型语言模型 (LLM) 的发展取得了显著的成果&#xff0c;并逐渐应用于多模态领域&#xff0c;例如视觉语言模型 (VLM)。VLM 旨在将 LLM 的强大能力扩展到视觉领域&#xff0c;使其能够理解和处理图像和文本信息&#xff0c;并完成诸如视觉问答、图像描…