【高阶数据结构】平衡二叉树(AVL)的删除和调整

news2024/11/18 20:15:01

🤡博客主页:醉竺

🥰本文专栏:《高阶数据结构》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多《高阶数据结构》点击专栏链接查看💛💜✨✨ 


目录

1. AVL树删除节点的步骤 

2. 删除过程示例(图解演示)

3. AVL树节点删除(图解+代码)

4. AVL树的性能


        上一篇文章的我们学习了平衡二叉树的4种插入方式,以及相对应的旋转调整,详情请点击链接:《平衡二叉树(AVL)的插入(4种旋转方法+精美图解+完整代码)》

        之所以把AVL树的插入和删除分两篇文章写,主要减少读者的阅读压力。但是由于《二叉树搜索树(BST)》、《平衡二叉树AVL》以及后续要学习的 红黑树(RBT) 之间是递增性的,所以如果是零基础的同学,建议可以先看我写的前几篇文章,每一篇文章我都详细通俗易懂地讲解了不同树地性质和相关操作。下面正式开始学习AVL树节点地删除:

1. AVL树删除节点的步骤 

        平衡二叉树删除某个节点的操作与二叉查找树删除某个节点的操作非常类似,但删除操作同样会使平衡二叉树失去平衡性。一旦因为删除某个节点导致平衡二叉树失衡,那么也要通过旋转使其恢复为平衡二叉树。删除操作的平衡性调整的实现代码有一定的复杂性,请你认真学习。我们先从删除过程的操作步骤开始说起。  

删除过程的三个步骤:

步骤一 在平衡二叉树中查找要删除的节点。

步骤二 针对所要删除的节点的子节点个数不同, 分别处理3类情况。 

注:步骤二的3种情况跟二叉搜索树节点的删除一模一样,下面只列出3种情形,具体地讲解可以看: 

步骤三:平衡性调整,这需要从被删除的节点向根节点回溯,也就是从下向上寻找。

  • 如果回溯发现所有节点都是平衡的,则不需要调整,因为这表示删除该节点并不影响二叉树的平衡性。

  • 如果回溯找到了第一个不平衡的节点(以该节点为根的这棵需要进行平衡性调整的子树,叫做“最小不平衡子树”,这在AVL树节点地插入一文中已详细讲解),这个不平衡节点的平衡因子为-2或2,需要分开讨论。

上述步骤暂时不理解也没关系,继续往后学习,下面有删除节点的例子。 

这里提出一个问题: 

AVL树节点的插入和删除在调整平衡因子过程中有什么不同? 
        平衡二叉树的插入只需要从插入节点之父向上检查,发现不平衡立即调整,一次调平衡即可。而删除操作则需要一直从删除节点之父向上检查,发现不平衡立即调整,然后继续向上检查,检查到树根为止。 

2. 删除过程示例(图解演示)

AVL树节点删除的具体步骤可以按照下面来,在演示代码之前我会举几个删除节点的例子,看完保证你会掌握 AVL节点的删除。接下来阅读的时候一定要比对着下面的具体步骤,这样可以更清晰地理解删除过程。

例1 

例2 

例3 

例4 

例5

例6 

2.选择RL型 


3. AVL树节点删除(图解+代码)

由上述删除具体步骤 “③找最小不平衡子树下,“个头”(高度)最高的儿子、孙子",和 “④根据孙子的位置,调整平衡(LL/RR/LR/RL)”,我们引出一个问题:在代码中如何找到个头最高的儿子和孙子呢?
答:我们找到最小不平衡子树根节点的时候,假如我们称该最小不平衡子树的根节点为 爷爷节点,则该节点的平衡因子一定是 -2 或者 2。

  1. 爷爷节点的平衡因子为-2(小于0)时,说明其左孩子的高度小于右孩子的高度,即“个头”高的儿子是其右孩子(儿子)。也说明删除的节点在爷爷节点的左侧。
  2. 反之,爷爷的平衡因子为 2(大于0)时,说明爷爷左孩子的高度大于右孩子的高度,即“个头”高的儿子是其左孩子(儿子)。 也说明删除的节点在爷爷节点的右侧。
  3. 同理,当儿子节点的平衡因子小于0时,说明儿子节点的左孩子高度小于右孩子的高度,即“个头”高的孙子是其右孩子(孙子)
  4. 儿子节点的平衡因子大于0时,说明儿子节点的左孩子高度大于右孩子的高度,即“个头”高的孙子是其左孩子(孙子)

上述 1 , 2 两种情形可以与 3, 4 相互组合,孙子节点相对于爷爷节点的位置:就有了 LL型,LR型、RR型和RL型。

平衡因子的更新和调整所有情况如下:
第一种,平衡因子(节点10)为-2,参考图1。这里你可能会碰到三类情况。 

  1. 如果其右孩子(图1左侧图节点12)的平衡因子为-1,则说明该右孩子的右子树(以节点13为根)更高,这种情形等同于RR型插入操作所要进行的平衡性调整,也就是要通过左旋转来恢复二叉树的平衡。

  2. 如果其右孩子(图1中间图节点12)的平衡因子为1,则说明该右孩子的左子树(以节点11为根)更高,这种情形等同于RL型插入操作所要进行的平衡性调整,也就是要通过先右后左旋转来恢复二叉树的平衡。

  3. 如果其右孩子(图1右侧图节点12)的平衡因子为0,则既可以通过“左旋转”又可以通过“先右后左旋转”来恢复二叉树的平衡。

对图1所示的三棵二叉树进行平衡性调整后的示意图如图2所示。


第二种情况,如果平衡因子(节点10)为2,参考图3。同样,我们还是会碰到三种情况。  

  1. 如果其左孩子(图3左侧图节点8)的平衡因子为-1,则说明该左孩子的右子树(以节点9为根)更高,这种情形等同于LR型插入操作所要进行的平衡性调整,也就是要通过先左后右旋转来恢复二叉树的平衡。

  2. 如果其左孩子(图3中间图节点8)的平衡因子为1,则说明该左孩子的左子树(以节点6为根)更高,这种情形等同于LL型插入操作所要进行的平衡性调整,也就是要通过右旋转来恢复二叉树的平衡。

  3. 如果其左孩子(图3右侧图节点8)的平衡因子为0,则即可以通过“右旋转”又可以通过“先左后右旋转”来恢复二叉树的平衡。

最后,对图3所示的三棵二叉树进行平衡性调整后的示意图如图4所示。


在代码实现中,找到实际需要被删除的结点后,我们先不进行实际的删除,而是先进行平衡因子的更新,不然后续更新平衡因子时特别麻烦(已经尝试过),而更新平衡因子时的规则与插入结点时的规则是相反的,更新规则如下:

  1. 删除的结点在parent的左边,parent的平衡因子- -。
  2. 删除的结点在parent的右边,parent的平衡因子+ +。

并且每更新完一个结点的平衡因子后,都需要进行以下判断:

  • 如果parent的平衡因子等于-1或者1,表明无需继续往上更新平衡因子了。
  • 如果parent的平衡因子等于0,表明还需要继续往上更新平衡因子。
  • 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

判断理由说明:

parent更新后的平衡因子分析

-1或1   
只有0经过++ / - -操作后会变成-1/1,说明原来parent的左子树和右子树高度相同,现在我们删除一个结点,并不会影响以parent为根结点的子树的高度,从而变化影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。

0  
只有-1/1经过++ / - -操作后会变成0,说明本次删除操作使得parent的左右子树当中较高的一棵子树的高度降低了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。

-2或2   
此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。

代码实现: 

	//删除函数
	bool Erase(const K& key)
	{
		//用于遍历二叉树
		Node* parent = nullptr;
		Node* cur = _root;
		//用于标记实际的删除结点及其父结点
		Node* delParentPos = nullptr;
		Node* delPos = nullptr;

		while (cur)
		{
			if (key < cur->_kv.first) //所给key值小于当前结点的key值
			{
				//往该结点的左子树走
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_kv.first) //所给key值大于当前结点的key值
			{
				//往该结点的右子树走
				parent = cur;
				cur = cur->_right;
			}
			else //找到了待删除结点
			{
				if (cur->_left == nullptr) //待删除结点的左子树为空
				{
					if (cur == _root) //待删除结点是根结点
					{
						_root = _root->_right; //让根结点的右子树作为新的根结点
						if (_root)
							_root->_parent = nullptr;
						delete cur; //删除原根结点
						return true; //根结点无祖先结点,无需进行平衡因子的更新操作
					}
					else
					{
						delParentPos = parent; //标记实际删除结点的父结点
						delPos = cur; //标记实际删除的结点
					}
					break; //待删除结点有祖先结点,需更新平衡因子
				}
				else if (cur->_right == nullptr) //待删除结点的右子树为空
				{
					if (cur == _root) //待删除结点是根结点
					{
						_root = _root->_left; //让根结点的左子树作为新的根结点
						if (_root)
							_root->_parent = nullptr;
						delete cur; //删除原根结点
						return true; //根结点无祖先结点,无需进行平衡因子的更新操作
					}
					else
					{
						delParentPos = parent; //标记实际删除结点的父结点
						delPos = cur; //标记实际删除的结点
					}
					break; //待删除结点有祖先结点,需更新平衡因子
				}
				else //待删除结点的左右子树均不为空
				{
					//替换法删除
					//寻找待删除结点右子树当中key值最小的结点作为实际删除结点
					Node* minParent = cur;
					Node* minRight = cur->_right;
					while (minRight->_left)
					{
						minParent = minRight;
						minRight = minRight->_left;
					}
					cur->_kv.first = minRight->_kv.first; //将待删除结点的key改为minRight的key
					cur->_kv.second = minRight->_kv.second; //将待删除结点的value改为minRight的value
					delParentPos = minParent; //标记实际删除结点的父结点
					delPos = minRight; //标记实际删除的结点
					break; //删除结点有祖先结点,需更新平衡因子
				}
			}//end-找到了待删除结点
		}//end-while

		if (delParentPos == nullptr) //delParentPos没有被修改过,说明没有找到待删除结点
		{
			return false;
		}

		//记录待删除结点及其父结点(用于后续实际删除)
		Node* del = delPos;
		Node* delP = delParentPos;

		//更新平衡因子
		while (delPos != _root) //最坏一路更新到根结点
		{
			if (delPos == delParentPos->_left) //delParentPos的左子树高度降低
			{
				delParentPos->_bf--; //delParentPos的平衡因子--
			}
			else if (delPos == delParentPos->_right) //delParentPos的右子树高度降低
			{
				delParentPos->_bf++; //delParentPos的平衡因子++
			}
			//判断是否更新结束或需要进行旋转
			if (delParentPos->_bf == 0)//需要继续往上更新平衡因子
			{
				//delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
				delPos = delParentPos;
				delParentPos = delParentPos->_parent;
			}
			else if (delParentPos->_bf == -1 || delParentPos->_bf == 1) //更新结束
			{
				break; //delParent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
			}
			else if (delParentPos->_bf == -2 || delParentPos->_bf == 2) //需要进行旋转(此时delParentPos树已经不平衡了)
			{
				if (delParentPos->_bf == -2)
				{
					if (delParentPos->_right->_bf == -1)
					{
						Node* tmp = delParentPos->_right; //记录delParentPos左旋转后新的根结点
						RotateL(delParentPos); //左单旋
						delParentPos = tmp; //更新根结点
					}
					else if (delParentPos->_right->_bf == 1)
					{
						Node* tmp = delParentPos->_right->_left; //记录delParentPos右左旋转后新的根结点
						RotateRL(delParentPos); //右左双旋
						delParentPos = tmp; //更新根结点
					}
					else //delParentPos->_right->_bf == 0
					{
						Node* tmp = delParentPos->_right; //记录delParentPos左旋转后新的根结点
						RotateL(delParentPos); //左单旋
						delParentPos = tmp; //更新根结点
						//平衡因子调整
						delParentPos->_bf = 1;
						delParentPos->_left->_bf = -1;
						break; //更正
					}
				}
				else //delParentPos->_bf == 2
				{
					if (delParentPos->_left->_bf == -1)
					{
						Node* tmp = delParentPos->_left->_right; //记录delParentPos左右旋转后新的根结点
						RotateLR(delParentPos); //左右双旋
						delParentPos = tmp; //更新根结点
					}
					else if (delParentPos->_left->_bf == 1)
					{
						Node* tmp = delParentPos->_left; //记录delParentPos右旋转后新的根结点
						RotateR(delParentPos); //右单旋
						delParentPos = tmp; //更新根结点
					}
					else //delParentPos->_left->_bf == 0
					{
						Node* tmp = delParentPos->_left; //记录delParentPos右旋转后新的根结点
						RotateR(delParentPos); //右单旋
						delParentPos = tmp; //更新根结点
						//平衡因子调整
						delParentPos->_bf = -1;
						delParentPos->_right->_bf = 1;
						break; //更正
					}
				}
				//delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
				delPos = delParentPos;
				delParentPos = delParentPos->_parent;
				//break; //error
			}
			else
			{
				assert(false); //在删除前树的平衡因子就有问题
			}
		}
		//进行实际删除
		if (del->_left == nullptr) //实际删除结点的左子树为空
		{
			if (del == delP->_left) //实际删除结点是其父结点的左孩子
			{
				delP->_left = del->_right;
				if (del->_right)
					del->_right->_parent = delP;
			}
			else //实际删除结点是其父结点的右孩子
			{
				delP->_right = del->_right;
				if (del->_right)
					del->_right->_parent = delP;
			}
		}
		else //实际删除结点的右子树为空
		{
			if (del == delP->_left) //实际删除结点是其父结点的左孩子
			{
				delP->_left = del->_left;
				if (del->_left)
					del->_left->_parent = delP;
			}
			else //实际删除结点是其父结点的右孩子
			{
				delP->_right = del->_left;
				if (del->_left)
					del->_left->_parent = delP;
			}
		}
		delete del; //实际删除结点
		return true;
	}

四种插入方式旋转方法调整:LL型插入(RotateR),RR型插入(RotateL),LR(RotateLR),RL(RotateRL) ,相关代码请看平衡二叉树的插入.

4. AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个结点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log^{n}。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但当一个结构经常需要被修改时,AVL树就不太适合了。

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

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

相关文章

记一次教学版内网渗透流程

信息收集 如果觉得文章写的不错可以共同交流 http://aertyxqdp1.target.yijinglab.com/dirsearch dirsearch -u "http://aertyxqdp1.target.yijinglab.com/"发现 http://aertyxqdp1.target.yijinglab.com/joomla/http://aertyxqdp1.target.yijinglab.com/phpMyA…

DialFRED基准:具有对话能力的具身智能Agent

目录 一、DialFRED数据集1.1 数据集规模与任务结构1.2 任务实例的构成1.3 人类标注的问答数据1.4 Oracle自动生成答案1.5 任务多样性与数据增强1.6 数据集的词汇多样性1.7 任务和环境的多样性 二、提问者-执行者框架2.1 框架概述2.2 提问者模型设计2.3 执行者模型设计2.4 强化学…

【读书笔记-《30天自制操作系统》-25】Day26

本篇仍然是围绕着命令行窗口做文章。首先优化命令行窗口的移动速度&#xff0c;然后增加多个命令行窗口功能。接着优化了命令行窗口的关闭&#xff0c;最后增加了两个命令start与ncst。 1. 优化命令行窗口移动速度 首先对命令行窗口的移动速度进行优化。主要的优化点有以下几…

WEB服务器——Tomcat

服务器是可以使用java完成编写&#xff0c;是可以接受页面发送的请求和响应数据给前端浏览器的&#xff0c;而在开发中真正用到的Web服务器&#xff0c;我们不会自己写的&#xff0c;都是使用目前比较流行的web服务器。 如&#xff1a;Tomcat 1. 简介 Tomcat 是一个开源的轻量…

二维数组的存放

今天我水的文章是二维数组的存放 二维数组的存放方式其实和一维数组没有区别&#xff0c;但如果想要更直观的了解&#xff0c;我们可以把它们的地址打印出来。 代码如下&#xff1a; #include <stdio.h> int main() {int arr[3][3];//二维数组&#xff0c;int数组类型…

【高效管理集合】并查集的实现与应用

文章目录 并查集的概念主要操作优化技术应用场景 并查集的实现基本框架并查集的主要接口总体代码 并查集的应用省份的数量等式方程的可满足性 总结 并查集的概念 并查集&#xff0c;也称为不相交集&#xff0c;是一种树形的数据结构&#xff0c;用于处理一些不相交集合的合并及…

ClickHouse | 查询

1 ALL 子句 2 ARRAY JOIN 使用别名 :在使用时可以为数组指定别名&#xff0c;数组元素可以通过此别名访问&#xff0c;但数组本身则通过原始名称访问 3 DISTINCT子句 DISTINCT不支持当包含有数组的列 4 FROM子句 FROM 子句指定从以下数据源中读取数据: 1.表 2.子…

建筑资质应该怎么选?

建筑资质是建筑企业承接工程项目的必备条件&#xff0c;它不仅关系到企业的市场竞争力&#xff0c;还直接影响到企业的经营效益。因此&#xff0c;选择适合自己企业的建筑资质至关重要。以下是一些选择建筑资质时需要考虑的关键因素&#xff1a; 1. 明确企业定位 首先&#x…

金融教育宣传月 | 平安养老险百色中心支公司开展金融知识“消保县域行”宣传活动

9月22日&#xff0c;平安养老险百色中心支公司积极落实国家金融监督管理总局关于开展金融教育宣传月活动的相关要求&#xff0c;联合平安人寿百色中心支公司共同组成了平安志愿者小队&#xff0c;走进百色市四塘镇百兰村开展了一场别开生面的金融消费者权益保护宣传活动。此次活…

如何给你的项目添加测试覆盖率徽章

看完我的测试教程之后&#xff0c;想必大家都能写出一个测试覆盖率极高的小项目了。测试覆盖率既然这么高&#xff0c;不秀一秀岂不是白瞎了&#xff0c;下面我们就来通过第三方服务来给你的项目加上测试覆盖率徽章&#xff0c;涉及到的内容有yaml配置&#xff0c;githubAction…

Vue下载pubsub-js中错误问题解决

错误&#xff1a; 解决方法&#xff1a; 执行&#xff1a; npm config set registry https://registry.npm.taobao.org我执行以上方法后安装成功

关于北斗卫星导航系统,你都了解多少?

北斗卫星导航系统&#xff08;简称“北斗系统”&#xff09;&#xff0c; 英文全称是&#xff1a;Beidou Navigation Satellite System&#xff08;简称&#xff1a;BDS&#xff09;&#xff0c; 研发 的 初衷 是中国着眼于国家安全和经济社会发展需要&#xff0c;选择自主研发…

Java类的生命周期-初始化阶段

Java类的生命周期-初始化阶段 前两篇讲述了类生命周期的加载阶段和连接阶段&#xff0c;那么本篇我们来讲最为重要的初始化阶段&#xff0c;借助字节码文件与大厂面试题更好的理解类的初始化 头篇提到&#xff0c;类的生命周期可疑将他分为五个阶段&#xff0c;本篇要讲述的就是…

RIP路由(已被淘汰)

一、rip 路由原理 RIP&#xff08;Routing Information Protocol&#xff0c;路由信息协议&#xff09;早期的动态路由协议&#xff0c;被广泛应用于TCP/IP网络中&#xff0c;尤其是在中小型网络中。基于距离矢量&#xff08;Distance-Vector&#xff09;算法来计算到达目的网络…

农场小程序带你走进生态农产品的世界

在快节奏的现代生活中&#xff0c;人们对食品安全的关注日益增强&#xff0c;对环境、健康农产品的需求也愈发迫切。然而&#xff0c;传统农产品市场往往信息不透明&#xff0c;消费者难以直接了解农产品的生长环境和生产过程&#xff0c;导致信任缺失。而农场小程序的出现&…

工程安全监测分析模型与智能算法模型方案

工程安全监测分析模型与智能算法模型 构建大坝安全监测智能分析模型&#xff0c;以大坝立体智能感知体系为依托&#xff0c;获取大坝变形、渗流渗压、环境变量等实时监测数据&#xff0c;作为模型输入&#xff0c;实现监测数据自动预处理、特征提取、误差分析、变化趋势分析等…

大模型增量训练--基于transformer制作一个大模型聊天机器人

针对夸夸闲聊数据集&#xff0c;利用UniLM模型进行模型训练及测试&#xff0c;更深入地了解预训练语言模型的使用方法&#xff0c;完成一个生成式闲聊机器人任务。 项目主要结构如下&#xff1a; data 存放数据的文件夹 dirty_word.txt 敏感词数据douban_kuakua_qa.txt 原始语…

Qt——如何创建一个项目

前言 本文主要通过实操带领大家来实现基础文件的操作&#xff0c;主要包括文件的打开&#xff0c;读取&#xff0c;写入&#xff0c;当然文件读写我们可以有几种不同的方式来进行操作&#xff0c;分别是文件流&#xff0c;字节流来进行的操作这里就需要两个类分别是文件流&…

迈威通信闪耀工博会,以创新科技赋能工业自动化

昨日&#xff0c;在圆满落幕的第24届中国国际工业博览会上&#xff0c;迈威通信作为工业自动化与智慧化领域的先行者&#xff0c;以“创新打造新质通信&#xff0c;赋能工业数字化”为主题精彩亮相&#xff0c;向全球业界展示了我们在工业自动化领域的最新成果与创新技术。此次…

elementUI表格中某个字段(state)使用计算属性进行转换为对应中文显示

代码案例&#xff1a; <template><el-table:data"tableData"style"width: 100%"><el-table-columnprop"date"label"日期"width"180"/><el-table-columnprop"name"label"姓名"wid…