数据结构---跳表

这篇具有很好参考价值的文章主要介绍了数据结构---跳表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

为什么会有跳表

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

跳表的原理

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

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

跳表的模拟实现

准备工作

首先每个节点的中存在一个变量用来存储数据,然后还需要一堆指针来记录下一个节点的位置,指针的数量不清楚而且又不止一个所以我们可以创建一个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:
数据结构---跳表,c++详解,数据结构,算法
在节点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数据结构---跳表,c++详解,数据结构,算法
插入之后就变成这样:
数据结构---跳表,c++详解,数据结构,算法
而数组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;
}

代码的运行结果如下:
数据结构---跳表,c++详解,数据结构,算法
可以看到代码的运行结果符合我们的预期。文章来源地址https://www.toymoban.com/news/detail-635426.html

效率比较

  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、哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力。

到了这里,关于数据结构---跳表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【高阶数据结构】跳表

    skiplist本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是 一样的,可以作为key或者key/value的查找模型。那么相比而言它的优势是什么的呢?这么等我 们学习完它的细节实现,我们再来对比。 skiplist 是由 William Pugh 发明的,最早出现于他在

    2024年02月16日
    浏览(27)
  • 【数据结构】3.跳表和散列

    跳表可以近似实现二分查找的效果 以下面长度为7的链表举例,该跳表通过3条链表进行存储。假设要查找元素80: 首先在第2级链表查找,因为80大于40,所以在第3个节点右侧查找 然后在第1级链表查找,因为80大于75,所以在第5个节点右侧查找 最后在第0级链表查找,找到元素

    2024年02月08日
    浏览(30)
  • 数据结构:跳表的原理和运用

    本篇总结的是跳表的相关内容 skiplist 本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为 key 或者 key/value 的查找模型 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图所示。这样所有新

    2024年02月22日
    浏览(35)
  • 10.Redis数据结构之跳表

    sortedSet是Redis提供的一个非常特别的数据结构,常用作排行榜等功能,将用户id作为value,关注时间或者分数作为score进行排序。 与其他数据结构相似,zset也有两种不同的实现,分别是zipList和(hash+skipList)。 规则如下: zipList满足以下两个条件 [score,value]键值对数量少于128个;

    2024年02月11日
    浏览(29)
  • [Redis] 数据结构zset压缩列表实现和跳表实现讲解

    😚一个不甘平凡的普通人,致力于为Golang社区和算法学习做出贡献,期待您的关注和认可,陪您一起学习打卡!!!😘😘😘 🤗专栏:算法学习 🤗专栏:Go实战 💬个人主页:个人主页 跳表问题 redis 有五种数据结构:string,hash,list,set,zset 压缩列表 或者 跳表 实现 压缩

    2024年02月05日
    浏览(39)
  • Redis从入门到精通【高阶篇】之底层数据结构跳表(SkipList)

    上个篇章回顾,我们上个章节我们学习了《Redis从入门到精通【高阶篇】之底层数据结构整数集(IntSet)详解》,我们从源码层了解整数集由一个头部和多个数据块组成。头部中存储了整数集的元素个数、编码方式和数据块的起始地址等信息。数据块中存储了实际的整型数据,当

    2024年02月09日
    浏览(38)
  • 数据结构与算法——数据结构有哪些,常用数据结构详解

    数据结构是学习数据存储方式的一门学科,那么,数据存储方式有哪几种呢?下面将对数据结构的学习内容做一个简要的总结。 数据结构大致包含以下几种存储结构: 线性表,还可细分为顺序表、链表、栈和队列; 树结构,包括普通树,二叉树,线索二叉树等; 图存储结构

    2024年02月15日
    浏览(45)
  • 数据结构KMP算法详解

    目录 1. KMP算法是什么? 2. KMP算法的由来 2.1 需要要解决的问题 2.2 一开始想到的方法 2.3 KMP算法诞生了 3.KMP算法的详解 4.KMP算法的实现 5.KMP算法的改进 KMP算法是一种改进的字符串匹配算法,即可以 快速的从主串中找到子串的算法 ,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人

    2024年02月12日
    浏览(48)
  • 【排序算法】数据结构排序详解

    前言: 今天我们将讲解我们数据结构初阶的最后一部分知识的学习,也是最为“炸裂”的知识---------排序算法的讲解!!!! 排序 :所谓排序,就是使一串记录,按照其中的某个或某些的大小,递增或递减的排列起来的操作。 稳定性 :假定在待排序的记录序列中,

    2023年04月08日
    浏览(37)
  • 数据结构-图搜索算法详解

    图搜索算法是数据结构和算法学科中的一个重要领域,它们用于在图中搜索顶点(节点)和边(连接节点的线)。图可以是有向的(边有方向)或无向的(边没有方向)。图搜索算法主要用于解决如路径查找、网络流分析等问题。下面详细介绍几种常见的图搜索算法。 深度优

    2024年04月28日
    浏览(22)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包