12.2旋转,SPLAY树的各种操作(SPLAY与AVL是两种BST)

Splay树和AVL树是两种不同的自平衡二叉搜索树实现。

1. 平衡条件:AVL树通过维护每个节点的平衡因子(左子树高度减去右子树高度)来保持平衡,要求每个节点的平衡因子的绝对值不超过1。Splay树则通过经过每次操作后将最近访问的节点调整到根节点的方式来保持平衡,而不依赖于平衡因子。

2. 平衡调整:AVL树在进行插入或删除操作时,可能需要通过旋转来调整树的结构以保持平衡。旋转的过程比较固定,需要进行左旋、右旋、双旋等操作。Splay树在进行操作时,将最近访问的节点通过一系列旋转操作调整到根节点,称为"伸展"操作。这种伸展操作将最近访问的节点置于更接近根节点的位置,从而实现了平衡。

3. 时间复杂度:AVL树保证了每个节点的左右子树高度差不超过1,因此可以保证所有操作的时间复杂度为O(logN),其中N是树的节点数量。Splay树的操作时间复杂度是均摊的,具体取决于节点的访问模式。对于频繁访问的节点,其操作时间复杂度可能接近O(1),但对于其他节点,可能会有较高的时间复杂度。

4. 动态性能:AVL树在插入、删除和查找操作的平均和最坏情况下都具有较好的性能,但可能会频繁地进行旋转操作。相比之下,Splay树对于最近访问的节点有更好的动态性能,因为它将这些节点调整到根节点位置,使得下一次访问更快。然而,对于没有被频繁访问的节点,Splay树的性能可能较差。

综上所述,AVL树和Splay树在平衡调整的策略、时间复杂度和动态性能方面有所不同。AVL树适用于对平衡性要求较高、操作频率较低的场景,而Splay树则适用于需要频繁访问最近访问节点的场景。

 树旋转

void rotate(int &cur,int f) {
    int son = TR[cur].ch[f];
    TR[cur].ch[f] = TR[son].ch[f ^ 1];
    TR[son].ch[f ^ 1] = cur;
    cur = son;
}

f表示cur结点的对应孩子,多一个son指针指向它;cur指向的是要旋转的结点,是被旋转下去的结点,son是被旋转上来的结点;即cur为原根节点,son为新根结点。但是cur,始终指向根节点位置,也就是树的根节点编号

原根结点与新根结点满足f关系,则新根结点之前的所有孩子也和原根结点满足f关系;旋转后,则原根结点与新根结点满足f^1关系,要让原根结点的f位置处更新,也要使其满足与原根节点的f关系,但是要和新根结点满足f^1关系,则只能选择新根结点的f^1处的孩子,接到原根节点f处。

旋转步骤:

  

void rotate(int x)
{
	int y=t[x].fa,z=t[y].fa,chk=get(x);
	t[y].ch[chk]=t[x].ch[chk^1];//1
	if(t[x].ch[chk^1])
		t[t[x].ch[chk^1]].fa=y;//2
	t[x].ch[chk^1]=y;//3
	t[y].fa=x;//4
	t[x].fa=z;//5
	if(z)
		t[z].ch[y==t[z].ch[1]]=x;//6
	maintain(y);
	maintain(x);
}

旋转后的性质

ABC。 

进行右旋后,右子树得到一个原根节点为一层,并且原根节点得到新根结点原右子树,故左子树高度至少增加一层;同样,左旋后,左子树的高度至少增加1

旋转时机

要进行左旋,说明右子树重;要进行右旋,说明左子树重。

这种重代表高度的失衡,表达为

即此时就一定需要旋转了。

接着判断重的子树的左子树与右子树高度关系

如果重的方向连续一样,那就做反方向一次调整;不然,就做一次调整,使之变为对应情况,再进行反方向一次调整

如果左子树的左子树高度比左子树的右子树高度大,就说明是一直左边很大,往右允(右旋)一下就行

 就是旋转前有个判断,如果LL,RR就旋一次就行;不然就先旋一次构成LL,RR再进行一次旋

所谓三点共线,就是父亲和儿子的类型相同,即都是左孩子或都是右孩子,不能一左一右

操作

旋转

void rotate(int x)
{
	int y=t[x].fa,z=t[y].fa,chk=get(x);
	t[y].ch[chk]=t[x].ch[chk^1];//1
	if(t[x].ch[chk^1])
		t[t[x].ch[chk^1]].fa=y;//2
	t[x].ch[chk^1]=y;//3
	t[y].fa=x;//4
	t[x].fa=z;//5
	if(z)
		t[z].ch[y==t[z].ch[1]]=x;//6
	maintain(y);
	maintain(x);
}

x表示被旋上去的结点,是新根结点,识别x是原根结点的什么孩子,做相应的旋转 

具体是左旋还是右旋取决于 chk 的值,如果 chk 为0,则进行右旋操作;如果 chk 为1,则进行左旋操作。

 SPLAY

将根节点旋转到根

void splay(int x)
{
	for(int f=t[x].fa;f=t[x].fa,f;rotate(x))
		if(t[f].fa)
			rotate(get(x)==get(f)?f:x);
	root=x;
}

子树大小

对于节点3来说,它的左子节点是2,右子节点是4。节点的重复次数为2,表示节点上存储的值3重复出现了两次。

对于节点2来说,它的左子节点为空,右子节点为空。节点的重复次数为1。

对于节点4来说,它的左子节点为空,右子节点是5。节点的重复次数为3,表示节点上存储的值4重复出现了三次。

对于节点5来说,它的左子节点为空,右子节点为空。节点的重复次数为2。

现在我们来计算节点的子树大小 sz

对于节点3来说,它的 sz 值应该等于左子树的 sz 值加上右子树的 sz 值,再加上节点自身的重复次数。左子树为空,右子树只有一个节点4,所以左子树的 sz 值为0,右子树的 sz 值为4。节点3的重复次数为2。因此,节点3的 sz 值为0 + 4 + 2 = 6。

同样的道理,对于节点2来说,它的 sz 值为左子树的 sz 值加上右子树的 sz 值,再加上节点自身的重复次数。左子树为空,右子树为空,节点2的重复次数为1。因此,节点2的 sz 值为0 + 0 + 1 = 1。

以此类推,可以计算出其他节点的 sz 值。

这个就是考虑到树中存储元素会有重复的,考虑到了重复的情况,就加了一个重复权值,即sz,sz越大,这个结点展开后的序列就越长,但是存储时就是一个值的结点

插入

旋转:将当前节点旋转到根

void insert(int k)
{
	if(!root)//若树为空
	{
		t[++tot].val=k;
		t[tot].cnt++;
		root=tot;
		maintain(root);
		return;
	}
	int cur=root,f=0;
	while(1)
	{
		if(t[cur].val==k)//1
		{
			t[cur].cnt++;
			maintain(cur);
			maintain(f);
			splay(cur);
			break;
		}
		f=cur;
		cur=t[f].ch[t[f].val<k];//2
		if(!cur)//3
		{
			t[++tot].val=k;
			t[tot].cnt++;
			t[tot].fa=f;
			t[f].ch[t[f].val<k]=tot;
			maintain(tot);
			maintain(f);
			splay(tot);
			break;
		}
	}
}

1. 首先检查树是否为空。如果树为空,则创建一个新节点,将其值设置为 `k`,重复次数 `cnt` 设置为1,并将其作为根节点。然后调用 `maintain` 函数维护根节点的信息,并返回。

2. 如果树不为空,则通过循环找到插入位置。初始化 `cur` 为根节点的索引,`f` 为当前节点的父节点的索引。

3. 在循环中,首先检查当前节点 `cur` 的值是否等于待插入的值 `k`。如果相等,则更新当前节点的重复次数 `cnt`,调用 `maintain` 函数维护当前节点和其父节点的信息,然后调用 `splay` 函数将当前节点旋转到根节点位置。这是为了保持伸展树的特性,即插入的节点被旋转到根节点位置。

4. 如果当前节点的值不等于待插入的值 `k`,则更新 `f` 为当前节点 `cur`,并根据 `k` 的大小决定往左子树还是右子树方向移动。即,如果 `k` 小于当前节点的值,则将 `cur` 更新为当前节点的左子节点索引;如果 `k` 大于当前节点的值,则将 `cur` 更新为当前节点的右子节点索引。

5. 如果当前节点 `cur` 为空(达到了叶子节点),则说明待插入的值不在树中,需要创建一个新节点,将其值设置为 `k`,重复次数 `cnt` 设置为1,并将其插入到当前节点 `f` 的对应子节点位置。然后调用 `maintain` 函数维护新插入节点和其父节点的信息,再调用 `splay` 函数将新插入节点旋转到根节点位置。

通过这些操作,可以将新的节点插入到伸展树中,并将其旋转到根节点位置,以维持树的平衡性和性能。

查询排名,根据元素返回排名

每沿着树往下走一次,就意味着左右子树所能容纳的元素数量就会少一个等级,即深度增加一次,而不是类似于区间一样容纳从开始到这里的所有元素,而只是和上个根节点一同构成的一个区间里的所有元素。所以在x大于当前结点时,也要加上当前结点的所有左子树大小,因为下一步是要往这个结点的右子树上走,走了之后深度加一,区间左端点就不再包括上个根节点的左子树,区间左端点就是上个根节点(因为上个根节点的所有右子树上元素都比根节点大)

int rnk(int x)
{
	int res=0,cur=root;
	while(1)
	{
		if(x<t[cur].val)//向左子树走,而不用累加答案,因为比x小的都在左子树
			cur=t[cur].ch[0];
		else
		{
			res+=t[t[cur].ch[0]].size;//累加左子树的size,因为左子树上的权值都小于x
			if(x==t[cur].val)//如果权值与x相等
			{
				splay(cur);//旋转
				return res+1;//“排名定义为比当前数小的数的个数+1”
			}
			res+=t[cur].cnt;//累加当前节点size,因为当前节点权值小于x
			cur=t[cur].ch[1];//右子树
		}
	}
}

查询数值,根据排名返回元素

之所以要减去左子树size以及当前结点cnt,是因为每个结点记录的都只是大小,子树的大小,自身的权值,而不是记录整体的一个排名

k<=0时说明已经找到,就是当前这个结点。=0时说明刚好找到,<0则说明当前这个结点有重复的,如1,1,1,1,查询到的是第2个或第3个1

int kth(int k)
{
	int cur=root;
	while(1)
	{
		if(t[cur].ch[0]&&k<=t[t[cur].ch[0]].size)//左子树存在且排名为k的值在左子树
			cur=t[cur].ch[0];
		else
		{
			k-=t[t[cur].ch[0]].size+t[cur].cnt;//将k改为在右子树的排名
			if(k<=0)//如果排名小于等于0,说明已经找到,直接返回
			{
				splay(cur);
				return t[cur].val;
			}
			cur=t[cur].ch[1];
		}
	}
}

前驱与后继

int pre()
{
	int cur=t[root].ch[0];//向左
	if(!cur)//如果已经到叶子结点
		return cur;
	while(t[cur].ch[1])//向右
		cur=t[cur].ch[1];
	splay(cur);//旋转
	return cur;
}
int nxt()
{
	int cur=t[root].ch[1];//向右
	if(!cur)
		return cur;
	while(t[cur].ch[0])//向左
		cur=t[cur].ch[0];
	splay(cur);//旋转
	return cur;
}

删除

先调用rnk(k)来定位值为K的结点,并将其调整到根节点

void del(int k)
{
	rnk(k);
	if(t[root].cnt>1)//1
	{
		t[root].cnt--;
		maintain(root);
		return;
	}
	if(!t[root].ch[0]&&!t[root].ch[1])//2
	{
		clear(root);
		root=0;
		return;
	}
	if(!t[root].ch[0])//3
	{
		int cur=root;
		root=t[root].ch[1];
		t[root].fa=0;
		clear(cur);
		return;
	}
	if(!t[root].ch[1])//4
	{
		int cur=root;
		root=t[root].ch[0];
		t[root].fa=0;
		clear(cur);
		return;
	}
	int cur=root;//5
	int x=pre();
	t[t[cur].ch[1]].fa=root;
	t[root].ch[1]=t[cur].ch[1];
	clear(cur);
	maintain(root);
}

需要注意,pre之后,x就成为了新根结点,通过cur保留一个指针指向原根节点。

原根节点的先序一定在左子树上,所以变为新根结点时一定是右旋,而且原根节点的先序一定是叶子结点,一定没有孩子,所以可以直接接。或者,先序成为新根结点后,其右子树一定一定是原根节点,因为是其先序,右子树比根大。

让原根节点的右孩子的父亲更新为新根结点,让新根结点的右孩子更新为原根节点的右孩子

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【隐私计算】VOLE (Vector Oblivious Linear Evaluation)学习笔记

近年来&#xff0c;VOLE&#xff08;向量不经意线性评估&#xff09;被用于构造各种高效安全多方计算协议&#xff0c;具有较低的通信复杂度。最近的CipherGPT则是基于VOLE对线性层进行计算。 1 VOLE总体设计 VOLE的功能如下&#xff0c;VOLE发送 Δ \Delta Δ和 b b b给send…

MySQL笔记-第03章_基本的SELECT语句

视频链接&#xff1a;【MySQL数据库入门到大牛&#xff0c;mysql安装到优化&#xff0c;百科全书级&#xff0c;全网天花板】 文章目录 第03章_基本的SELECT语句1. SQL概述1.1 SQL背景知识1.2 SQL语言排行榜1.3 SQL 分类 2. SQL语言的规则与规范2.1 基本规则2.2 SQL大小写规范 …

Linux系统安装Python3环境

1、默认情况下&#xff0c;Linux会自带安装Python&#xff0c;可以运行python --version命令查看&#xff0c;如图&#xff1a; 我们看到Linux中已经自带了Python2.7.5。再次运行python命令后就可以使用python命令窗口了&#xff08;CtrlD退出python命令窗口&#xff09;。 2…

MySQL笔记-第06章_多表查询

视频链接&#xff1a;【MySQL数据库入门到大牛&#xff0c;mysql安装到优化&#xff0c;百科全书级&#xff0c;全网天花板】 文章目录 第06章_多表查询1. 一个案例引发的多表连接1.1 案例说明1.2 笛卡尔积&#xff08;或交叉连接&#xff09;的理解1.3 案例分析与问题解决 2. …

详解原生Spring当中的事务

&#x1f609;&#x1f609; 学习交流群&#xff1a; ✅✅1&#xff1a;这是孙哥suns给大家的福利&#xff01; ✨✨2&#xff1a;我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 &#x1f96d;&#x1f96d;3&#xff1a;QQ群&#xff1a;583783…

如何从T-N曲线判断电机选对了没有

我的知乎原文&#xff1a;https://zhuanlan.zhihu.com/p/670156320? 如果你是一个刚入行的电机工程师&#xff0c;刚刚参加了一个新产品的开发&#xff0c;在众多电机供应商中让你去挑选一款合适的电机&#xff0c;该从哪个角度去入手呢&#xff1f; 今天这篇文章就从T-N曲线…

llama2.c推理

模型图 代码及分析 不需要考虑任何mask问题&#xff0c;直接通过矩阵计算求出下三角矩阵每个元素的值即可&#xff0c;不需要额外添加mask之类的。 temperature0&#xff08;确定性&#xff09;的时候&#xff0c;模型推理每次都取概率最大的&#xff08;从而导致同样的输入…

4.grid_sample理解与使用

pytorch中的grid_sample 文章目录 pytorch中的grid_samplegrid_samplegrid_sample函数原型实例 欢迎访问个人网络日志&#x1f339;&#x1f339;知行空间&#x1f339;&#x1f339; grid_sample 直译为网格采样&#xff0c;给定一个mask patch&#xff0c;根据在目标图像上的…

css实现正六边形嵌套圆心

要实现一个正六边形嵌套圆心&#xff0c;可以使用CSS的::before和::after伪元素以及border-radius属性。以下是具体的解析和代码&#xff1a; 使用::before和::after伪元素创建正六边形。设置正六边形的背景色。使用border-radius属性使正六边形的内角为60度。在正六边形内部创…

基于Springboot的在线问卷调查系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的在线问卷调查系统(有报告)。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring…

初步认识结构体

hello&#xff0c;hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习结构体&#xff0c;并跟大家一边做题一边进行学习和理解。感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教&#xff01; 如果本篇文章对你有帮助&#xff0c;还请…

【arduino库之TroykaDHT(针对DHT系列温湿度传感器)】

该库允许您从 DHT 系列传感器读取温度和湿度。 该库允许获取以摄氏度、开尔文和华氏度为单位的相对湿度和温度数据。支持的传感器&#xff1a;DH11、DHT21、DHT22。 TroykaDHT库的的使用非常简单&#xff0c;它包含7个函数&#xff1a; begin //初始化接口&#xff0c;做好…

Course2-Week2-神经网络的训练方法

Course2-Week2-神经网络的训练方法 文章目录 Course2-Week2-神经网络的训练方法1. 神经网络的编译和训练1.1 TensorFlow实现1.2 损失函数和代价函数的数学公式 2. 其他的激活函数2.1 Sigmoid激活函数的替代方案2.2 如何选择激活函数2.3 为什么需要激活函数 3. 多分类问题和Soft…

Redis 分布式锁测试

一、前提依赖&#xff08;除去SpringBoot项目基本依赖外&#xff09;&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId> </dependency><!-- 配置使用redis启动…

移动端APP自动化测试框架-UiAutomator2基础

很早以前&#xff0c;我用uiautomatorjava实践过Android APP自动化测试&#xff0c;不过今天要提的不是uiautomator&#xff0c;而是uiautomator2。听起来uiautomator2像是uiautomator的升级版&#xff0c;但是这两款框架仅仅是名字上比较相似&#xff0c;实际上没有任何关联。…

BiseNet实现遥感影像地物分类

遥感地物分类通过对遥感图像中的地物进行准确识别和分类&#xff0c;为资源管理、环境保护、城市规划、灾害监测等领域提供重要信息&#xff0c;有助于实现精细化管理和科学决策&#xff0c;提升社会治理和经济发展水平。深度学习遥感地物分类在提高分类精度、自动化程度、处理…

最长连续序列代码中的细节解读

最长连续序列 一、题目概述 给定一个未排序的整数数组 nums &#xff0c;找出数字连续的最长序列&#xff08;不要求序列元素在原数组中连续&#xff09;的长度。 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 原题地址&#xff1a;https://leetcode.cn/problems/l…

前端开发学习 (四) 自定义按键修饰符

一、 v-on的按键修饰符 按键修饰符&#xff0c;通俗的来说就是监听键盘输入的事件&#xff0c; 在Vue 中允许为 v-on 在监听键盘事件时添加按键修饰符 修饰符用途.enter当在输入框按下回车时触发.tab当按下tab时触发.delete当按下删除键&#xff08;通常是键盘上的Delete键&am…

软件工程 课后题 选择 查缺补漏

在一张状态图中只能有一个初态&#xff0c;而终态则可以没有&#xff0c;也可以有多个 所有的对象可以成为各种对象类&#xff0c;每个对象类都定义了一组 方法 通过执行对象的操作可以改变对象的属性&#xff0c;但它必须经过 消息 的传递 UML应用于 基于对象的面向对象的方…

主动张罗,持续改善!震坤行客服团队再获「全国青年文明号」殊荣

主动张罗&#xff0c;持续改善&#xff01;震坤行客服团队再获「全国青年文明号」殊荣 近日&#xff0c;第21届全国青年文明号集体评选结果揭晓。由共青团中央、最高人民法院、国家发展改革委、工业和信息化部等23家全国创建青年文明号活动组委会成员单位联合印发《关于命名第2…