数据结构---跳表

news2024/12/23 20:29:19

目录标题

  • 为什么会有跳表
  • 跳表的原理
  • 跳表的模拟实现
    • 准备工作
    • find函数
    • insert函数
    • erase函数
  • 测试
  • 效率比较

为什么会有跳表

在前面的学习过程中我们学习过链表这个容器,这个容器在头部和尾部插入数据的时间复杂度为O(1),但是该容器存在一个缺陷就是不管数据是否有序查找数据是否存在的时间复杂度都是O(N),我们只能通过暴力循环的方式查找数据是否存在,尽管数据是有序的我们也不能通过二分查找的形式去查找一个数据,那么为了解决这个问题就有人提出来了跳表这个容器。

跳表的原理

之前学习的链表结构如下:
在这里插入图片描述
那么跳表的思路就是:每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点
在这里插入图片描述
这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概只有原来的一半。比如说要查找元素19,首先就比较头结点最上面指针指向的节点(节点中的元素为6)是否大于19,如果大于就跳转到该节点,那么很明显这里是大于的所以跳转到节点6上,然后接着比较元素6最上面的指针指向的节点(节点9)是否大于19,很明显是晓得所以跳转到元素9上,然后接着就来到节点17,因为节点17最上面的节点指向的元素是21比19大,那么这时就不能往该指针指向的节点进行跳转,我们得比较该指针下面的指针(17号节点中下方指针)指向的节点,该节点指向的值刚好为19所以就找到了指定元素,那么这就是跳表查找元素的原理
在这里插入图片描述

可是上面的查找过程并没有提高很大的效率最好的情况也是O(N/2),所以为了进一步的提高效率我们在第二层新产生的链表上,继续为每相邻的两个节点升高一层增加一个指针,从而产生第三层链表:
在这里插入图片描述
那么这时查找元素19就会变的更快,经过一次比较就来到了节点9,然后就跳转到节点17,最后就来到了节点19:
在这里插入图片描述
可以看到再添加一层节点就会使得节点查找的效率变的更高,那么同样的道理只要链表的数据个数够长我们还可以添加多层这样以此类推直到无法添加节点为止,那么这个时候进行第一次比较就可以过滤掉一半的元素,再经历一次比较又可以过滤掉1/4的元素,再经历一次比较就可以过滤掉1/8的元素等等,这样一直循环下去我们就可以发现此时查找元素的时间复杂度为O(logN),但是这里存在一个问题虽然这样的结构能够带来logN的效率,但是该效率是用整齐的结构换过来的,如果在使用的过程中对元素进行插入和删除这种结构就会被破坏,比如说最高的层数为10,如果我要往中间的节点插入一个节点的话这个节点的层数是多少呢?1到10都可以对吧,但是插入之后链表就没办法保持之前的规律(每相邻的节点抬高一层),如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。skiplist的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数,这样就好处理多了。
在这里插入图片描述
那这里就存在一个问题如果时随机分配层数的话那如何来保证效率呢?随机数有很多很多:1是随机数1w也是随机数,并且还可能出现多个很大的随机数,比如说当前有100个节点但是有99个节点的高度为100,但是我们需要很多高层的节点吗?好像不需要对吧,理论上来说层数越高的节点出现的概率应该越小,所以为了提高跳表的空间效率和时间效率我们给跳表设计一个最大层数maxLevel的限制,其次再设置一个多增加一层的概率P,也就是说每个节点的高度至少为1每升高一层的概率p,那么一个节点的高度为1的概率是:1-P(本来就有一个层高度不增加的概率为1-p)所以高度为1的概率是1-P,同样的道理高度为1增高一层的概率为p不增加的概率为1-p所以层数为二的概率就是p*(1-p),节点层数为3的概率就是p*p*(1-p),这样以此类推就不难发现节点层数越高出现的概率越小:
在这里插入图片描述
那么这就是跳表的原理接下来我们来看看如何实现跳表。

跳表的模拟实现

准备工作

首先每个节点的中存在一个变量用来存储数据,然后还需要一堆指针来记录下一个节点的位置,指针的数量不清楚而且又不止一个所以我们可以创建一个vector对象来存储指针,那么这里我们就可以创建一个类来描述指针,该类的构造函数就需要两个参数一个参数表示节点的层数另外一个参数表示节点存储的数据,然后在构造函数里面就对数组进行初始化将每个指针都初始化为空,那么这里的代码如下:

template<class T>
struct ListNode
{
	typedef ListNode<T> Node;
	ListNode(T val,int size)
		:_nextV(size,nullptr)
		,_val(val)
	{}
	T _val;
	vector<Node*> _nextV;
};

然后在跳表类里面就存储一个指针变量用来记录头结点然后创建另个变量用来记录当前节点的最大层数和增加层数的概率,在构造函数里面将这个指针指向一个新创建的节点,节点的高度为1存储的值为元素的默认构造,并且后面的代码我们要用到随机数这个东西,所以在构造函数里面还得添加一个时间戳,那么这里的代码如下:

template<class T>
class Skiplist
{
public:
	typedef ListNode<T> Node;
	Skiplist()
	{
		srand(time(0));
		_head = new Node(T(), 1);
	}

private:
	Node* _head;
	int _maxLevel = 32;
	double _p = 0.5;
};

find函数

find函数是这个类的关键因为这里我们不仅要找到这个元素还得找到直线这个元素的其他节点,比如我们要查找元素21:
在这里插入图片描述
在节点21前面存在很多节点指向21,比如说9号节点17号节点19号节点这三个节点都指向节点21,那么find函数不仅要判断21是否存在还得返回指向21号节点的节点,之所以这么做是为了方便后面的insert函数和erase函数,但是这里存在一个问题我们知道了哪个节点指向21,但是不知道是这些节点中的哪个指针指向21?所以这个时候就得将数组中的下表结合起来,因为一个高度的指针只有一个指针指向某个节点,所以我们就可以用数组的下表来进行辅助判断,如果数组中下表为1的元素中记录的节点是9号节点的话就表明9号节点的中下表为1的指针指向了该节点,那么这样find函数不仅可以查询节点是否存在还可以找到哪些位置指向该节点,这样可以方便后面的插入函数和删除函数,那么首先这个函数需要一个T类型的参数,然后返回值是vector类型并且vector中的元素类型是Node*

vector<Node*> find(const T& target)
{}

然后在函数里面就可以创建一个vector,将其大小初始化为头结点的长度,然后创建一个变量level用来存储头结点中数组的个数,因为我们要不停比较节点中的值,所以还得创建一个Node类型的指针用来指向当前寻找的节点,然后就可以创建一个while循环,因为循环的目的是将每一个指向目标节点的节点都记录下来,所以循环结束的条件就是level大于等于0,那么这里代码如下:

vector<Node*> find(const T& target)
{
	int level = _head->_nextV.size()-1;//这里是下表所以要减一
	vector<Node*> tmp(level+1,_head);//这里要加一,并且每个元素都指向头结点
	Node* cur = _head;
	while (level >= 0)
	{
	}
}

循环里面我们就要做出判断:当前cur指向的节点的level层指针指向的元素是否小于target,如果小于就将cur指向level层指针指向的元素,如果大于target或者当前的指针指向的元素为空就说明找到了指向插入位置或者要删除元素的前一个节点,那么这个时候就将该节点的地址填入数组中的level下表然后将level的值减减即可,那么这里额度代码如下:

vector<Node*> find(const T& target)
{
	int level = _head->_nextV.size()-1;//这里是下表所以要减一
	vector<Node*> tmp(level+1,_head);//这里要加一,并且每个元素都指向头结点
	Node* cur = _head;
	while (level >= 0)
	{
		// 目标值比下一个节点值要大,向右走
		// 下一个节点是空(尾),目标值比下一个节点值要小,向下走
		if (cur->_nextV[level] != nullptr && cur->_nextV[level]->_val < target)
		{
			// 向右走
			cur = cur->_nextV[level];
		}
		else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val >= target)
		{
			// 更新level层前一个
			tmp[level] = cur;
			// 向下走
			level--;
		}
	}
	return tmp;
}

当然这样的find函数还是太难用了,所以我们还是添加一个简化版的find函数,那么这里的思路是一样的我们就直接看代码:

bool search(T 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;
}

insert函数

当出现重复值时插入可能失败所以insert函数的返回值为bool,函数的参数为T类型

bool insert(T num)
{
}

那么在函数的开始就得创建一个数组用来接收find函数的返回值,然后我们得生成一个随机数用来记录数的高度,那么这里我们还得写一个函数用来随机创建树的高度,那么这里的代码如下:

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

我们知道rand函数生成的数据大小是有范围的,那么我们就可以利用这个范围来生成随机高度,有了这个函数之后我们就可以得到高度,然后new出来节点并将节点的高度设为函数的返回值,那么这里的代码如下:

bool insert(T num)
{
	vector<Node*> prevV = FindPrevNode(num);
	int height = RandomLevel();
	Node* newnode = new Node(num, height);
}

因为插入一个节点之后,可能该节点的高度会刷新整个链表的高度,所以我们得进行判断如果新创建出来的节点高度大于头结点的高度就得对头节点和上面的prevV进行扩长,那么这里就得添加一个if语句:

bool insert(T num)
{
	vector<Node*> prevV = FindPrevNode(num);
	int height = RandomLevel();
	Node* newnode = new Node(num, height);
	if (height > _head->_nextV.size())
	{
		_head->_nextV.resize(height, nullptr);
		prevV.resize(height, _head);
	}
}

然后就要将prevV数组中的节点高度个元素的指针指向进行改变,让新插入的节点指向他们之前指向的节点,然后让这些节点指向新插入的节点,比如说下面的图片要插入元素15在这里插入图片描述
插入之后就变成这样:
在这里插入图片描述
而数组prevV中刚好记录所有指向该位置的节点,那么这里就可以创建一个while循环进行更改,那么完整的代码如下:

bool insert(T num)
{
	vector<Node*> prevV = find(num);
	int height = RandomLevel();
	Node* newnode = new Node(num, height);
	if (height > _head->_nextV.size())
	{
		_head->_nextV.resize(height, nullptr);
		prevV.resize(height, _head);
	}
	for (int i = 0; i < height; i++)
	{
		newnode->_nextV[i] = prevV[i]->_nextV[i];
		prevV[i]->_nextV[i] = newnode;
	}
}

erase函数

erase函数的实现思路也是一样的首先得判断一下当前要删除的元素是否存在如果不存在的话就直接返回false,如果存在的话先创建一个数组和find函数一起记录指向该节点的节点,然后跟insert函数一样修改节点中指针的指向,将指向该节点的指针指向该节点的下一个,那么这里的代码如下:

bool erase(T 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;
	}
}

但是这里存在一个问题,如果将最高节点删除了那头节点是不是也得进行变化呢?所以这里还得查看一下头结点中不为空指针的数量,然后将其长度进行缩小,那么完整的代码如下:

bool erase(T 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;
		else
			break;
	}
	_head->_nextV.resize(i + 1);
	return true;
}

测试

为了方便测试我们可以添加一个打印函数:

void Print()
{
	Node* cur = _head;
	while (cur)
	{
		printf("%2d\n", cur->_val);
		// 打印每个每个cur节点
		for (auto e : cur->_nextV)
		{
			printf("%2s", "↓");
		}
		printf("\n");
		cur = cur->_nextV[0];
	}
}

然后用下面的代码来进行测试:

int main()
{
	Skiplist<int> tmp;
	tmp.insert(11);
	tmp.insert(5);
	tmp.insert(6);
	tmp.insert(12);
	tmp.insert(2);
	tmp.insert(3);
	tmp.insert(7);
	tmp.insert(17);
	tmp.insert(19);
	cout << tmp.search(11) << endl;
	cout << tmp.search(20) << endl;
	tmp.erase(11);
	cout << tmp.search(11) << endl;
	cout << endl;
	tmp.Print();
	return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到代码的运行结果符合我们的预期。

效率比较

  1. skiplist相比平衡搜索树(AVL树和红黑树)对比,都可以做到遍历数据有序,时间复杂度也差不多。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/847153.html

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

相关文章

sysstat安装与使用

官方文档 http://sebastien.godard.pagesperso-orange.fr/documentation.html sysstat安装 1.下载源码 https://github.com/sysstat/sysstat 2.编译安装 tar xvf sysstat-xxx.tar.gz ./configure make -j 16 make install3.测试 iostatsysstat使用 sysstat 包包含许多商…

基于Flask的模型部署

基于Flask的模型部署 一、背景 Flask&#xff1a;一个使用Python编写的轻量级Web应用程序框架&#xff1b; 首先需要明确模型部署的两种方式&#xff1a;在线和离线&#xff1b; 在线&#xff1a;就是将模型部署到类似于服务器上&#xff0c;调用需要通过网络传输数据&…

css小练习:案例6.炫彩加载

一.效果浏览图 二.实现思路 html部分 HTML 写了一个加载动画效果&#xff0c;使用了一个包含多个 <span> 元素的 <div> 元素&#xff0c;并为每个 <span> 元素设置了一个自定义属性 --i。 这段代码创建了一个简单的动态加载动画&#xff0c;由20个垂直排列的…

ESD接地实时监控系统有哪些功能

ESD接地实时监控系统是一种用于监测和维护静电放电&#xff08;ESD&#xff09;接地的设备和软件系统。静电放电事件可能会对敏感电子元件、设备或工作环境造成损害&#xff0c;因此对ESD接地进行有效的监控至关重要。 ESD接地实时监控系统主要包括以下几个方面的功能&#xf…

第五次作业 运维高级 构建 LVS-DR 集群和配置nginx负载均衡

1、基于 CentOS 7 构建 LVS-DR 群集。 LVS-DR模式工作原理 首先&#xff0c;来自客户端计算机CIP的请求被发送到Director的VIP。然后Director使用相同的VIP目的IP地址将请求发送到集群节点或真实服务器。然后&#xff0c;集群某个节点将回复该数据包&#xff0c;并将该数据包…

成功解决ubuntu-22.04的sudo apt-get update一直卡在【0% [Waiting for headers]】

成功解决ubuntu-22.04的sudo apt-get update一直卡在【0% [Waiting for headers]】 问题描述解决方案 问题描述 在下载安装包的时候一直卡在0% [Waiting for headers]&#xff0c;报错信息如下&#xff1a; Get:1 file:/var/cudnn-local-repo-ubuntu1804-8.5.0.96 InRelease […

中科驭数亮相DPU峰会,分享HADOS软件生态实践和大数据计算方案,再获评“匠芯技术奖”

又是一年相逢时&#xff0c;8月4日&#xff0c;第三届DPU峰会在北京开幕&#xff0c;本届峰会由中国通信学会指导&#xff0c;江苏省未来网络创新研究院主办&#xff0c;SDNLAB社区承办&#xff0c;以“智驱创新芯动未来”为主题&#xff0c;沿袭技术创新、生态协同的共创效应&…

vue 路由页面跳转

从index.vue跳转到data.vue index.vue <el-table-column label"客户数" align"center" :show-overflow-tooltip"true"><template slot-scope"scope"><router-link :to"/system/enterprise-data/index/ scope.ro…

人工智能术语翻译(五)

文章目录 摘要QRST 摘要 人工智能术语翻译第五部分&#xff0c;包括Q、R、S、T开头的词汇&#xff01; Q 英文术语中文翻译常用缩写备注Q FunctionQ函数Q-LearningQ学习Q-NetworkQ网络Quadratic Loss Function平方损失函数Quadratic Programming二次规划Quadrature Pair象限…

NSS [MoeCTF 2022]baby_file

NSS [MoeCTF 2022]baby_file 题目源码直接给了 使用data伪协议发现被ban了。 那就换一种伪协议php://filter&#xff0c;猜测flag在同目录下flag.php中或根目录下/flag中 php://filter/readconvert.base64-encode/resourceflag.php读取文件源码&#xff08;针对php文件需要ba…

华为路由器:IPSec加密GRE通道(GRE over IPsec)

IPSec加密GRE通道 由于GRE隧道不提供安全性保障&#xff0c;使用ipsec加密gre隧道是现网中比较常用的VPN部署&#xff0c;它的加密方式分为两种&#xff1a; 可以使用IPsec来加密隧道进行传输&#xff0c;叫做IPsec over GRE&#xff1b; 加密数据流后从隧道传输&#xff0c;…

OLED透明屏拼缝技术:创新的显示解决方案

引言&#xff1a;OLED透明屏作为一种创新的显示技术&#xff0c;已经在各个领域展现出了巨大的潜力。而其中的拼缝技术更是为OLED透明屏的应用带来了全新的可能性。 对此&#xff0c;尼伽便大家具体介绍一下OLED透明屏拼缝技术的概念、优势以及应用领域&#xff0c;并探讨其在…

Git仓关联多个远程仓路径

前言 Git仓如果需要将代码push到多个仓&#xff0c;常用的做法是添加多个远程仓路径&#xff0c;然后分别push。这样虽然可以实现目的&#xff0c;但是需要多次执行push指令&#xff0c;很麻烦。 本文介绍关联多个远程仓路径且执行一次push指令的方法&#xff1a;git remote …

python教学资源百度网盘,python教程百度网盘资源

大家好&#xff0c;本文将围绕最全python教程百度网盘分享展开说明&#xff0c;python教程百度网盘资源是一个很多人都想弄明白的事情&#xff0c;想搞清楚python教学资源百度网盘需要先了解以下几个事情。 Python在近几年越来越受追捧&#xff0c;很多童鞋或者职场小伙伴想要提…

PotPlayer播放时、拖动播放条,CPU占用率高、卡顿

鼠标右击播放界面&#xff0c;滤镜/解码器管理 > 视频解码器 > 内置解码器/DXVA设置 > 勾选使用硬件加速&#xff0c;确定&#xff0c;关闭播放器再重新打开即可&#xff1b; 其他播放软件同理。 高级设置&#xff1a;https://www.hao4k.cn/thread-26475-1-1.html

【车道线】TwinLiteNet 复现过程全纪录

目录 1、下载代码 2、解压缩 3、建立conda环境 4、验证环境 5、复制环境 6、安装依赖 7、测试 1、下载代码 代码地址&#xff1a;https://github.com/chequanghuy/TwinLiteNet 2、解压缩 3、建立conda环境 我个人先建立起一个基础环境&#xff0c;然后复制为twinlitenet…

aardio + customPlus 显示图片演示

看效果&#xff1a; 上代码&#xff1a; import win.ui; /*DSG{{*/ var winform win.form(text"aardio customPlus 显示图片演示 by 光庆";right927;bottom607) winform.add( button{cls"button";text"下一页";left664;top536;right794;bott…

模板初阶以及string类使用

模板初阶以及string类使用 模板的简单认识1.泛型编程2.函数模板模板的原理图函数模板格式函数模板实例化非模板函数和模板函数的匹配原则 3.类模板类模板的定义格式类模板的实例化 string1.string简介2.string常用的接口 题目练习1.字符串相加2.字符串里面最后一个单词的长度3.…

Openlayers实战:使几何图形适配窗口

Openlayers开发的项目中,有一种应用非常重要,就是绘制或者显示出几何图形后,让几何图形居中并适配到窗口下,这样能让用户很好的聚焦到所要看的内容中去。 这里使用了fit的这个view 的方法,具体的操作请参考示例源代码。 效果图 源代码 /* * @Author: 大剑师兰特(xiaozh…

DPR-100-N-H-24插装式直动可调式减压阀

DPR-100-N-H-2,DPR-100-N-S-24,DPR-100-N-H-9,DPR-100-N-H-24,DPR-100-N-S-9,DPR-100-N-H-2直动式可调型电磁阀被设计用于调节副回路常压。 动作状况 DPR-100 稳态时&#xff0c;弹赞栓3打开&#xff0c;允许介质在1和2之间双向流动。当1处达到预设压力时&#xff0c;螺线节流…