C++--AVL树的插入,详解四种旋转规则(结尾附源代码链接)

news2024/11/30 2:32:49

AVL树的插入

  • 前言
    • 左单旋
    • 右单旋
    • 左右双旋
    • 右左双旋
    • 检查是否这颗树是否是AVL树

前言

AVL树可以说是对二叉搜索树的优化,我们来看二叉树搜索树的下一面一种特殊情况:
在这里插入图片描述
当我们插入的数是上面的情况时,二叉树搜索树的特点就形同虚设了,这就相当于一个长度为N的单链表了,那么时间复杂度就是O(N)了。
那么AVL树为了处理这种情况的发生呢,就应运而生。

先来大致的说一下AVL是怎么处理才能避免出现这种情况的。AVL树主要是通过几种旋转的方式,控制了整颗树的任意结点的高度差不超过1,如果超过1,就会发生旋转。

AVL树的定义:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树 。
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1。

AVL树的结点定义:
在上面提到了平衡因子是左右子树的高度差的绝对值,为了代码的实现的更明确,我们采用右子树的高度减去左子树的高度

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	pair<K, V> _kv;
	int _bf;//平衡因子(该结点的左右子树的高度差),右子树的高度减去左子树的高度

	AVLTreeNode(const pair<K, V>& kv)
		: _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

因为在高度不满足条件的时候,涉及到旋转,而旋转就必须要有一个指针指向前一个结点,所以我们在定义结点的时候,将结点定义成了三叉链的结构。
再提一句就是关于这个pari<K, V> _kv。我们在实现的时候,完全可以根据自己的场景需要来决定是否使用pair。如果你仅仅需要一个值的话,是可以只提供一个模板参数的。

在正式的谈旋转之前,我们要先将能触发旋转的场景全部分析出来,主要的就是四种大场景。
其中左单旋和右单旋都是比较简单的。

左单旋

那么下面我们就用画图的方式来看一下如何触发左单旋。
在这里插入图片描述
上图的a b c矩形代表的是各种满足AVL的结点的情况,因为不同的情况有很多很多种,所以这里就以抽象图来观察我们的旋转的情况,这种抽象图也是大佬在观察了很多种情况总结出来的。

在上图中,当我们要在c结点插入一个新结点的时候(先不用考虑增加b的情况,这种情况是会引用发更为复杂的旋转的情况,下面会具体分析),c的高度就会增加1,变成h+1高度。c结点的高度的变化会直接影响其父亲结点的高度差,也就是60结点的高度差会由0变成1。60的高度的更新,又会影响其父亲结点的高度差的变化,当c的高度增加1的时候,增颗右子树的高度就会由h+1变成h+2,这时根节点30的左右子树的高度差就由1变成了2。
如下图所示:
在这里插入图片描述
此时根节点的高度差不满足绝对值小于1了,这时候的解决办法就是左旋转一下。
过程如下:

  • 将60的左子树给30做右子树。
  • 30做60的左子树。
    结果如下图所示:
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/512a29d0b3c3441cb9630
    11e4c0b9cd9.png)
    大家可以想一下为什么可以这样做?提示:二叉搜索树的特点是什么?

旋转完成之后,30和60的平衡因子需要再重新更新一下。
代码如下:
parent就是引发旋转的那个结点,上图中就是30。

void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		//1.
		parent->_right = subRL;
		subR->_left = parent;

		//2.更改parent指向
		if (subRL != nullptr)
			subRL->_parent = parent;

		Node* ppNode = parent->_parent;//在更改parent的_parent之前记录下parent的父结点
		parent->_parent = subR;
		//3.还需要进行判断,该parent是否是整个树的根节点还是子树的根节点
		if (ppNode == nullptr)
		{
			//parent是整个树的根节点
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
				ppNode->_left = subR;
			else
				ppNode->_right = subR;

			subR->_parent = ppNode;
		}
		
		//4.更新平衡因子
		subR->_bf = parent->_bf = 0;
	}

大家把变量名带上再画一张旋转的图就更容易理解代码的逻辑。

右单旋

右单旋其实和左单旋可以说是完全对称的结构,下面给出右单旋的所示图,过程就不再解释了,和左单旋并没有什么本质上的区别。
在这里插入图片描述
旋转完成之后,记得更新平衡因子的值。
以下是代码:

void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//1.
		parent->_left = subLR;
		subL->_right = parent;

		//2.更改parent指向
		if (subLR != nullptr)
			subLR->_parent = parent;

		Node* ppNode = parent->_parent;
		parent->_parent = subL;

		//3.还需要进行判断,该parent是否是整个树的根节点还是子树的根节点
		if (ppNode == nullptr)
		{
			//parent是整个树的根节点
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if(ppNode->_left == parent)
				ppNode->_left = subL;
			else
				ppNode->_right = subL;

			subL->_parent = ppNode;
		}
		//4.更新平衡因子
		subL->_bf = parent->_bf = 0;
	}

下面是两种单旋图的所示图,放在一起对比大家再来看,是真的没什么本质的区别:
在这里插入图片描述

左右双旋

接下来的双旋的场景,说难也不难,说简单也不简单哈哈!
还是同样的我们以抽象图来画图,这里提一句大家可以将抽象图画成具体的实例图去走旋转,只不过实例图的场景实在是太多,不太适合拿出来举例子,也不具有说服力,这四种旋转的抽象图就是包含了各种场景的四种分类。
如果不理解直接记住也行的。

还是先给出一颗AVL树,如下所示:
在这里插入图片描述
此时如果我们在60结点的左子树或者右子树下插入新的结点,引发的高度差变化就如下图所示:
在b或者c插入引发的是同一种双旋,但是虽然是同一种双旋,但是会导致最后的更新的平衡因子不同,所以在进行旋转之前我们要事先记录下,60结点的平衡因子,在后序的更新平衡因子的时候,我们会用到这一点
在这里插入图片描述
这时候的根节点的做右子树的高度差不再满足条件,就会触发旋转,这种场景仅仅一次单旋肯定是没法解决的,所示我们就需要双旋了。

第一次单旋:先对30结点进行左单旋,结果如下图所示:
在这里插入图片描述
第二次单旋:对90结点进行右单旋,结果如下图所示:
在这里插入图片描述
左旋和右旋的规则上面已经介绍过了,可以看出,双旋也就是两次单旋组合的,只不过在最后的旋转完成之后,需要对一些结点的平衡因子进行更新,更新平衡因子和单旋的时候不一样,所示在实现双旋的代码的时候,对于旋转,只需要复用之前写的单旋的代码,自己最后写一下关于平衡因子的更新即可。

代码如下:

void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;//根据这个结点的平衡因子来确定新形成的AVL树的平衡因子

		RotateL(parent->_left);
		RotateR(parent);

		//无论那种情况,subL(根)的平衡因子都是0
		subLR->_bf = 0;
		if (bf == 0)
		{
			parent->_bf = subL->_bf = 0;
		}
		else if (bf == -1)
		{
			//结合画抽象图来看平衡因子怎么更新!!!!!!!!!!!!!!!!!--易错点
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

右左双旋

右左双旋和左右双旋的关系呢,和左单旋和右单旋的关系是一样的,也是对称的关,只不过最后的平衡因子的更新还是需要根据画图来进行判断。

以下是右左双旋的过程示意图:
在这里插入图片描述
代码:

void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		subRL->_bf = 0;
		if (bf == 0)
		{
			parent->_bf = subR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 1)
		{
			parent->_bf =-1;
			subR->_bf = 0;
		}
		else
			assert(false);
	}

左右双旋和右左双旋的对比示意图:
在这里插入图片描述

检查是否这颗树是否是AVL树

最后我们需要写一个函数,来检查用我们上面实现出来的 AVL树是满足要求的,即是一颗正确的AVL树。
值得一提的是,我们不能使用平衡因子来检查这颗树是否是正确的AVL树,因为平衡因子的更新也是我们自己写的,如果说因为我们自己的写的平衡因子有错误,而没有检查出来这颗树是有问题的,那么就无法保证改树是AVL树了。

思路如下:
既然平衡因子用不了,那么我们可以考虑直接计算出每个结点的左右子树的高度差,直接通过高度差来判断每个结点的高度差是否小于2。如果说不满足,那么就是我们实现的有问题,如果全部都检查完了,就说明我们实现的代码是能够构造出一颗正确的AVL树的。

代码如下:

bool isBalance()
	{
		return _isBalance(_root);
	}
private:
	bool _isBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		int leftHT = Height(root->_left);
		int rightHT = Height(root->_right);
		int diff = rightHT - leftHT;

		if (diff != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}
		//所有的结点都满足AVLTree这颗树才是AVLTree
		return abs(diff) < 2
			&& _isBalance(root->_left)
			&& _isBalance(root->_right);
	}
	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		return max(Height(root->_left), Height(root->_right)) + 1;
	}

测试用例及结果:
在这里插入图片描述
测试函数返回结果为真,结果正确。!

以下是完整的AVL树代码链接:
https://gitee.com/WXK-Tom/c-data-structure/tree/master/AVLTree/AVLTree

欢迎大家评论留言!

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

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

相关文章

ChatGpt 2步制作流程图与思维导图,你确定不来看一下吗?

什么&#xff1f;你还不会使用ChatGpt。推荐下面这篇文章 ChatGPT保姆级教程&#xff0c;一分钟学会使用ChatGPT&#xff01; - 掘金 (juejin.cn) 如果没有谷歌账号推荐直接买一个&#xff0c;因为你在中国注册谷歌账号&#xff0c;被谷歌查到&#xff0c;也是使用不了ChatGp…

企业数字化转型过程中面临最大的挑战和问题是什么?

无论组织规模如何&#xff0c;业务的敏捷性、弹性以及生产力的高低都是决定其发展运营成功与否的关键因素。而一个良好的数字化转型战略则是企业发展进步的有力助推器。 麦肯锡称&#xff0c;借助数字化转型&#xff0c;可以实现 20% 至 50% 的经济收益和 20% 至 30% 的客户满…

【Spring Cloud Alibaba】Nacos的安装与介绍以及Nacos集群的安装

欢迎来到 Nacos 的世界&#xff01; Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称&#xff0c;一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性…

shell构建基本脚本

构建基本脚本 使用多个命令 一次使用多个命令&#xff0c;把它们放在一行&#xff0c;使用’;隔开 [rootmyserver ~]# date ; who; ls Sun May 14 23:39:34 CST 2023 root pts/0 2023-05-14 23:31 (192.168.10.1) anaconda-ks.cfg initial-setup-ks.cfg创建shel…

JavaScript学习-DOM事件进阶

事件流 事件流和两个阶段说明 事件流指的是事件完整执行过程中的流动路径 说明:假设页面里有个div&#xff0c;当触发事件时&#xff0c;会经历两个阶段&#xff0c;分别是捕获阶段、冒泡阶段 简单来说:捕获阶段是 从父到子 冒泡阶段是从子到父 或者说从大的往下的是捕获&am…

阿里云服务器如何安装宝塔面板?

使用阿里云服务器安装宝塔面板教程&#xff0c;阿里云服务器网以CentOS操作系统为例&#xff0c;安装宝塔Linux面板&#xff0c;先远程连接到云服务器&#xff0c;然后执行宝塔面板安装命令&#xff0c;系统会自动安装宝塔面板&#xff0c;安装完成后会返回面板地址、账号和密码…

前端有必要掌握TypeScript吗,答案十分肯定

本文首发自「慕课网」&#xff08;www.imooc.com&#xff09;&#xff0c;想了解更多IT干货内容&#xff0c;程序员圈内热闻&#xff0c;欢迎关注"慕课网"或慕课网公众号&#xff01; 作者&#xff1a;一飞同学 | 慕课网讲师 近几年&#xff0c;前端技术发展越来越迅…

亚马逊云科技助力医疗与生命科学行业,提供高性能计算(HPC)

2023年4月27日&#xff0c;亚马逊云科技医疗与生命科学行业峰会召开。会议上&#xff0c;亚马逊云科技大中华区战略业务发展部总经理顾凡表示&#xff1a;“亚马逊云科技不仅提供覆盖全球的云基础设施以及超过200大类的云服务&#xff0c;更重要的是&#xff0c;我们深刻了解行…

Agisoft Metashape 卫星图像处理

Agisoft Metashape 利用卫星图像创建三维模型 文章目录 Agisoft Metashape 利用卫星图像创建三维模型前言一、添加图像二、对齐图像三、构建 DEM四、生成平铺模型前言 Agisoft Metashape Professional 支持处理带RPC参数的立体卫星图像。支持的卫星数据有:Beijing-3A, Ikonos…

Flash钓鱼->CS上线(免杀过火绒、360等)

先看结果 访问钓鱼页面: 点击立即升级即把马儿下载下来了 这个马儿是rar压缩的&#xff0c;做成的rar解压自启动&#xff0c;所以是个exe的文件&#xff0c;然后这里为了像一点&#xff0c;把图标给改了 双击运行&#xff0c;查看效果&#xff1a; 首先CS是没东西的 解压路…

UNITY3D 5V5网游开发案例教程

文章连载更新中&#xff0c;可以提前领取素材进行预习&#xff0c;自学 素材领取&#xff1a;私信发送 领取RPG网络开发教材 这里写目录标题 游戏玩法这门课适合哪些人学习学完了能达到什么效果项目准备基础系统战斗系统同步设计精讲社交系统副本系统优化项目准备正文美术准备&…

服务(第二十三篇)mysql-mha

1、什么是 MHA MHA&#xff08;Master High Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切换的过程中最大…

UnityWebSocket | 双端通信支持Text/Binary

跳转官方仓库地址 有多个项目使用该库&#xff0c;平台有PC、WebGL。 一、说明 1&#xff09;原理 WebSocket是H5提供的一种浏览器与服务器进行全双工通讯的网络技术&#xff0c;属于应用层协议。数据通常在两个站&#xff08;点对点&#xff09;之间进行传输&#xff0c;按照…

动态规划:01背包理论基础 二维dp

1.确定dp数组以及下标的含义 对于背包问题&#xff0c;有一种写法&#xff0c; 是使用二维数组&#xff0c;即dp[i][j] 表示从下标为[0-i]的物品里任意取&#xff0c;放进容量为j的背包&#xff0c;价值总和最大是多少。[0-i]物品任取放到容量为j的背包中得到的最大价值为dp[i…

想要上手playwright?看这篇文章就够了!(文章有点长,建议收藏)

本文系统地介绍了playwright的基础概念&#xff0c;架构&#xff0c;安装过程&#xff0c;编码demo&#xff08;python实例&#xff09;&#xff0c;常用API&#xff0c;录制脚本的方法以及playwright在无头模式的应用和其与selenium的对比。相信大家认真阅读本文后一定会对pla…

2023年5月广州/西安/成都/深圳产品经理认证NPDP报名

产品经理国际资格认证NPDP是新产品开发方面的认证&#xff0c;集理论、方法与实践为一体的全方位的知识体系&#xff0c;为公司组织层级进行规划、决策、执行提供良好的方法体系支撑。 【认证机构】 产品开发与管理协会&#xff08;PDMA&#xff09;成立于1979年&#xff0c;是…

【数据结构】从头到尾全解析双向链表

在之前我们已经讲过< 单链表 >了,单链表查找上一个结点的时间复杂度为O&#xff08;n&#xff09;&#xff0c;尾插时也要遍历一次链表也是O&#xff08;n&#xff09;&#xff0c;因为我们每次都要从头开始遍历找,为了克服这单向性的缺点&#xff0c;我们就有了双向链表…

软件测试专业应届生应如何提高职场竞争力

一&#xff1a;巩固专业知识 背景&#xff1a;笔者已经做了几年的打工人&#xff0c;以个人经验给软件测试专业应届生一些建议。 推荐需要掌握的知识&#xff1a; 1、软件测试基础知识&#xff08;软件生命周期每个阶段工作需了解&#xff09; 2、熟悉SQL/MySQL/Oracle数据库&…

D8加密狗使用教程

D8 加密锁 1.VsCode 安装中文扩展包(1) 打开 VsCode&#xff0c;点击左侧扩展.(2) 输入Chinese&#xff0c;会自动搜索&#xff0c;点击第一个中文简体扩展&#xff0c;点击安装(3) 重启VsCode 2. D8调试服务程序 - 只运行3. 自动安装 yttool&#xff08;1&#xff09;VsCode 打…

杭钢集团:以用友iuap为数智底座的数智化转型之路

近日&#xff0c;一年一度的用友BIP技术大会圆满召开。来自行业领先企业的CIO/CDO、生态伙伴、开发者、分析师、媒体等共聚北京用友产业园&#xff0c;了解最新技术发展趋势、探讨行业热点话题。会上&#xff0c;杭钢集团总经理助理施永益分享了杭钢集团基于用友BIP-iuap平台推…