C++14:AVL树

news2024/10/7 14:28:13

由于二叉搜索树在某些特定的情况下会退化成单叉树,为了解决这个问题,保证二叉搜索树能在绝大多数情况下保持高速搜索,G.M. Adelson-Velsky和E.M. Landis这两位俄国数学家提出了AVL树的概念,也就是高度平衡的搜索二叉树。

AVL树平衡大体逻辑:其具体的实现逻辑则是借助检查平衡因子来实现搜索二叉树的平衡,当平衡因子失衡的时候则旋转。

平衡因子:衡量当前节点的左右子树高度差的变量,当一个新节点插入右树则+1,插入左树则-1。所以左右子树高度之差(即平衡因子)的绝对值不超过1(-1/0/1),当超过1或-1时即达到失衡。

AVL树的实现

 节点结构的定义:


	template<class K,class V>
	struct AVLNode
	{
		pair<K, V> _kv;         kv结构
		AVLNode<K,V>* _left;    左节点
		AVLNode<K,V>* _right;   右节点
		AVLNode<K, V>* _parent; 父节点
		int _bf;                平衡因子

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

AVL树节点的插入

 AVL树的插入节点则比搜索二叉树更加复杂,其整体的实现逻辑顺序如下:

1.以搜索二叉树的插入逻辑完成最基本的节点插入,即比当前节点大就放入右子树,比当前节点小就放入左子树

2.检查并更新平衡因子,插入的节点是右边则当前节点的父节点的平衡因子+1,反之则-1,假如父节点的平衡因子是0则不需要再向上检查,若是1或者-1则需要向上更新平衡因子,最坏的情况则是更新到根。

3.失衡时旋转,当父节点的平衡因子等于2或者-2时,此时的AVL树已经失衡,需要旋转,具体的旋转分为多种情况,下文再细致讨论。

旋转情况

 具体的旋转情况比较复杂,所以使用抽象图来概括整体的情况。旋转有4种情况。

  • 情况1:新节点插入较高左子树的左侧,右单旋

 既然两个节点高度都加1时会触发左旋,那么我们以a子树的高度+1为例,画出旋转过程。旋转的本质是降低树的高度,在不破坏搜索二叉树的属性的情况下交换子树。

旋转过程中还需要注意一些细节:

  1. 如果是根节点,旋转完成后,要更新根节点
  2. 如果是子树,可能是某个节点的左子树,也可能是右子树
  3. 40节点的右孩子可能存在,也可能不存在
  4. 50可能是根节点,也可能是子树

 那么右单旋的代码如下:

		void RotateR(Node* parent)
		{
			Node* pparent = parent->_parent;
			Node* cur = parent->_left;

			parent->_left = cur->_right;
			//如果cur的左子树不等于空,才链接过去
			if (cur->_right)
				cur->_right->_parent = parent;

			parent->_parent = cur;
			cur->_right = parent;


			if (pparent == nullptr)
			{
				_root  = cur;
				_root->_parent = nullptr;
			}
			else
			{
				//pparent的孩子发生变动,判断是左子树还是右子树
				//若原先变动前的parent是pprant的左树
				if (pparent->_left == parent)
					pparent->_left = cur;
				else
					pparent->_right = cur;

				cur->_parent = pparent;

			}
  • 情况2:新节点插入较高右子树的右侧:左单旋

 此时以50为轴点进行旋转

 同情况一的代码逻辑相同,只不过指针的指向被更换了。

		void RotateL(Node* parent)
		{
			//旋转
			//parent变量所指向的节点一定是旋转轴点
			//	parent->right = parent->_right->_left
			//	那么parent的右孩子的_left指向parent, 也就是
			//	parent->_right->left = parent
			//	但还是需要额外处理一个问题,假如这次旋转只是处理了一个子树时,parent的right还需要更换祖宗
			//	所以还要一个pparent
			Node* pparent = parent->_parent;
			Node* cur = parent->_right;

			parent->_right = cur->_left;
			//如果cur的左子树不等于空,才链接过去
			if (cur->_left)
				cur->_left->_parent = parent;

			cur->_left = parent;
			parent->_parent = cur;

			//等于空,旋转了根节点
			if (pparent == nullptr)
			{
				_root = cur;
				_root->_parent = nullptr;
			}
			else//不等于空,旋转了一个子树
			{
				//pparent的孩子发生变动,判断是左子树还是右子树
				//若原先变动前的parent是pprant的左树
				if (pparent->_left == parent)
					pparent->_left = cur;
				else
					pparent->_right = cur;

				cur->_parent = pparent;

			}

			//旋转完毕,还需要更新平衡因子
			parent->_bf = cur->_bf = 0;
		}
  •  情况3:新节点插入较高左子树的右侧---左右双旋:先左单旋再右单旋
     

 本情况也就是情况1的另一边,由于此时单次旋转已无法解决问题,需要左右双旋转。

为什么需要左右双旋?

 为了更好的解释这个问题,需要将b节点的形状具象化,而非抽象化,b

 此时并不需要旋转,假如此时b节点的高度+1,AVL树又会进入失衡状态

 

 此时假如我们再次同情况一进行右旋转,会发现无法解决问题

 

树的整体形状从一个右折线被旋转成了左折线。

所以此时我们需要使用左右双旋,先将整体从折线变为直线,再进行旋转降低高度。

如何左右双旋?以上图为例,b的高度增加

注:此图中的60节点数值有误,应为30                                                                                   

 这样,整个AVL树可以再次进入平衡,那么假如换成d的高度增加,其实也是相同的旋转方法。只不过子树的位置变动了

 

		void RotateLR(Node* parent)
		{
			Node* subL = parent->_left;
			Node* subLR = subL->_right;

			int bf = subLR->_bf;

			RotateL(parent->_left);
			RotateR(parent);
			//左右双旋结束后,还需要更新正在这条折线上的平衡因子
			//平衡因子的更新情况有三种,因为造成左右双旋的原因是因为新增加的节点造成了折线式的失衡,才需要先左旋再右旋
			//那么针对折线底端的那个节点,既然是它造成了折线失衡,那么就需要处理当前节点的三种失衡情况,
			//1.当前节点增加在了它的右树,使其平衡因子+1
			//2.当前平衡因子增加在了它的左树,使其平衡因子-1
			//3.当前新增的节点就是其本身,平衡因子为0
			if (bf == -1) // subLR左子树新增
			{
				subL->_bf = 0;
				parent->_bf = 1;
				subLR->_bf = 0;
			}
			else if (bf == 1) // subLR右子树新增
			{
				parent->_bf = 0;
				subL->_bf = -1;
				subLR->_bf = 0;
			}
			else if (bf == 0) // subLR自己就是新增
			{
				parent->_bf = 0;
				subL->_bf = 0;
				subLR->_bf = 0;
			}
			else
			{
				assert(false);
			}

		}
  • 情况4:新节点插入较高右子树的左侧---右左双旋:先右单旋再左单旋
     

 情况四也就是情况二的变体

 旋转过程同情况三类似

 

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

			int bf = subRL->_bf;


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

			if (bf == -1) // subLR左子树新增
			{
				subR->_bf = 1;
				parent->_bf = 0;
				subRL->_bf = 0;
			}
			else if (bf == 1) // subLR右子树新增
			{
				parent->_bf = -1;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
			else if (bf == 0) // subLR自己就是新增
			{
				parent->_bf = 0;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
			else
			{
				assert(false);
			}
		}

 总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
        当pSubR的平衡因子为1时,执行左单旋
        当pSubR的平衡因子为-1时,执行右左双旋
2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
        当pSubL的平衡因子为-1是,执行右单旋
        当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

 根据以上结论,整理逻辑即可写出来AVL树的插入函数

		bool Insert(const pair<K,V> &kv)
		{
			//如果树为空,则创建一个节点
			if (_root == nullptr)
			{
				_root = new Node(kv);
				return true;
			}

			// 如果不为空,则找寻插入的位置,以Key作为插入根据,对比Key的大小
			//先检查当前插入的值应该往哪去
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (kv.first > cur->_kv.first)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (kv.first < cur->_kv.first)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					//相等,插入失败
					return false;
				}
			}

			//找到对应位置,新建节点。
			cur = new Node (kv);

			if (parent->_kv.first < cur->_kv.first)
			{
				parent->_right = cur;
				cur->_parent = parent;
			}
			else
			{
				parent->_left = cur;
				cur->_parent = parent;
			}

			//新建完节点之后,要更新平衡因子
			//插入右树则+1,插入左树则-1,当父亲节点的平衡因子为0的时候停止,最多修正至根
			while (parent)
			{
				//判断平衡因子该加还是该减少
				if (cur == parent->_left)
					parent->_bf--;
				else
					parent->_bf++;



				//移动完毕,平衡因子会有三种情况。等于0,不动,等于1,说明有变动,向上移动继续调整,最坏情况直到根节点
				//父亲的平衡因子等于0,不必再更新,直接break
				if (parent->_bf == 0)
				{
					break;
				}
				//更新之后进入不平衡状态,需要向上移动,因为造成了变动,还需要向上更新节点的平衡因子
				else if (parent->_bf == 1 || parent->_bf == -1)
				{
					cur = parent;
					parent = parent->_parent;
				}
				else if(parent->_bf == 2 || parent->_bf == -2)
					//此时AVL树已经失衡,需要旋转来修正
				{
					//旋转分为左旋转和右旋转,当更改来自右树时,也就是parent的bf==2,其右数的bf==1时,左旋转
					//右树失衡,左旋转
					if (parent->_bf == 2 && cur->_bf == 1)
						RotateL(parent);
					//左树失衡,右旋转
					else if (parent->_bf == -2 && cur->_bf == -1)
						RotateR(parent);
					//假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者 - 2,分以下情况考虑
					//	1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
					//	当pSubR的平衡因子为1时,执行左单旋
					//	当pSubR的平衡因子为 - 1时,执行右左双旋
					//	2. pParent的平衡因子为 - 2,说明pParent的左子树高,设pParent的左子树的根为pSubL
					//	当pSubL的平衡因子为 - 1是,执行右单旋
					//	当pSubL的平衡因子为1时,执行左右双旋
					//	旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
					else if (parent->_bf == -2 && cur->_bf == 1)
					{
						RotateLR(parent);
					}
					else if (parent->_bf == 2 && cur->_bf == -1)
					{
						RotateRL(parent);
					}
					else
						assert(false);

					break;

				}
				else//如果出现超过2的平衡因子,说明AVL树已经严重失衡,直接断死,不需要在做处理,整棵树的逻辑肯定出了问题
				{
					assert(false);
				}
			}

			return true;

		}

AVL树的验证

为了验证这颗AVL树是否真的达到了我们想要实现的功能,我们还需要实现一个检测函数来检查其高度,不能检查平衡因子,毕竟平衡因子是我们设定并更新的,假如我们只是简单的检查平衡因子,则有监守自盗的嫌疑,所以参考学习二叉树学习的一个求取二叉树的高度,我们实现两个函数走一个递归检测AVL树。

求取当前节点的左右子树高度

		int Height(Node* root)
		{
			if (root == nullptr)
				return 0;

			int left = Height(root->_left) + 1;
			int right = Height(root->_right) + 1;

			return  max(left,right);
		}

利用高度,同平衡因子比较

		bool IsBalance(Node* root)
		{
			if (root == nullptr)
				return true;
			int leftHeight = Height(root->_left);
			int rightHeight = Height(root->_right);


			if (rightHeight - leftHeight != root->_bf)
			{
				cout << root->_kv.first << "平衡因子异常" << endl;
				return false;
			}

			return abs(rightHeight - leftHeight) < 2
				&& IsBalance(root->_left)
				&& IsBalance(root->_right);
		}

这样,一颗AVL树就成功的实现了基本的功能

删除较为复杂,不记述

 

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

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

相关文章

ACM - 数据结构 - 基础(数组模拟链表 / 栈 / 队列 + 字典树 + 并查集 + 堆 + 哈希)

数据结构 一、线性表1、单链表模板题&#xff1a;AcWing 826. 单链表 2、双链表模板题 AcWing 827. 双链表 3、栈数组模拟栈模板 AcWing 828. 模拟栈逆波兰简版模板例题1、逆波兰表达式&#xff1a;HDU 1237 简单计算器&#xff08;写得有点复杂&#xff09; 4、队列数组模拟队…

接口自动化测试可以使用的几个常用的框架

接口自动化测试可以使用以下几个常用的框架&#xff1a; 1、pytest pytest是一个用于Python编写单元测试和功能测试的框架。它提供了简洁的语法、灵活的扩展性和丰富的插件&#xff0c;可以帮助开发人员高效地编写测试用例&#xff0c;并快速定位和解决问题。 以下是pytest的…

数据结构刷题(三十一):1049. 最后一块石头的重量 II、完全背包理论、518零钱兑换II

一、1049. 最后一块石头的重量 II 1.思路&#xff1a;01背包问题&#xff0c;其中dp[j]表示容量为j的背包&#xff0c;最多可以背最大重量为dp[j]。 2.注意&#xff1a;递推公式dp[j] max(dp[j], dp[j - stones[i]] stones[i]);本题中的重量就是价值&#xff0c;所以第二个…

边缘计算盒子适合用于哪些场景?

边缘计算盒子适用于在智慧工地、智慧工厂、智慧园区和智慧城管等场景中可以实现多种算法功能&#xff0c;以下是一些应用和实现算法功能&#xff1a; 智慧工地&#xff1a;实时视频监控与分析&#xff1a;边缘计算盒子可以处理实时监控视频流&#xff0c;进行人员和车辆识别、…

OpenPCDet系列 | 5.PointPillars模型前向传播完整流程

文章目录 前向传播流程1. 模型初始化2. 模型训练前向传播前向传播流程 这里以PointPillars模型为例,在PointPillars模型中主要划分了以下4个主要的模块,以下某块首先会在build_network中进行初始化,然后在具体前向传播时通过forward函数进行。下面进行区分。 PointPillars…

新颖拓扑指纹助力虚拟筛选:ToDD革新计算机辅助药物发现之路

编译 | 于洲 今天我们介绍由Novartis集团的Novartis与德克萨斯大学达拉斯分校的Baris Coskunuzer为第一作者发表在NeurIPS 2022会议上的工作&#xff0c;文章介绍了一种新的虚拟筛选方法——ToDD模型&#xff0c;该方法使用了多参数持久性同调&#xff08;MP&#xff09;来生成…

wx.request get请求重定向问题

微信小程序wx.request请求&#xff0c;重定向问题。 背景 在开发微信小程序项目的时候&#xff0c;使用wx.request请求一个公共接口&#xff0c;结果请求被重定向多次之后&#xff0c;返回失败&#xff0c;而且没有 code 码&#xff0c;只有一个 errMsg: request:fail。 Netw…

OpenShift - 使用 Ansible Automation Platform 纳管 OpenShift Virtualization 虚机

《OpenShift / RHEL / DevSecOps 汇总目录》 说明&#xff1a;本文已经在支持 OpenShift 4.12 的 OpenShift 环境中验证 本文所用到的运行环境和配置方法请先参照以下 blog 准备&#xff1a; 《OpenShift 4 - 安装部署 Ansible Automation Platform 4.x 控制台》《OpenShift …

React 组件

文章目录 React 组件复合组件 React 组件 本节将讨论如何使用组件使得我们的应用更容易来管理。 接下来我们封装一个输出 “Hello World&#xff01;” 的组件&#xff0c;组件名为 HelloMessage&#xff1a; React 实例 <!DOCTYPE html> <html> <head> &…

css中的grid高频布局

1.需求 1.一个父级容器内有n个子元素; 2.每个子元素最小宽度是100px&#xff1b; 3.每个子元素最大宽度根据屏幕宽度自适应&#xff1b; 4.每个子元素的宽度保持同宽&#xff1b; 5.每个元素之间有间隔&#xff0c;每一行的两边不留间隙&#xff0c;每一列的上下不留间隙&…

2.V853支持WIFI和有线网卡

100ASK-V853-PRO开发板支持WIFI和有线网卡 0.前言 ​ 通过上一章节&#xff0c;我们已经成功下载Tina SDK包&#xff0c;完成编译并烧写Tina Linux系统&#xff0c;接下来展示100ASK_V853-PRO开发部如何通过WIFI和有线网卡进行上网。 全志Linux Tina-SDK开发完全手册&#x…

02-mysql升级篇(rpm方式+压缩包升级)

文章目录 升级方式一、二进制方式安装1、下载mysql-5.7.42安装包&#xff08;mysql-5.7.37升级mysql-5.7.42&#xff09;2、备份数据库、my.cnf文件&#xff0c;停止mysql服务&#xff08;重要&#xff09;3、查看当前数据库版本3、上传 mysql-5.7.42-1.el7.x86_64.rpm-bundle.…

SpringMVC 详解

文章目录 一、SpringMVC简介1、什么是MVC2、什么是SpringMVC3、SpringMVC的特点 二、HelloWorld1、开发环境2、创建maven工程a>添加web模块b>打包方式&#xff1a;warc>引入依赖 3、配置web.xmla>默认配置方式b>扩展配置方式 4、创建请求控制器5、创建springMVC…

关于太阳黑子数的查询

太阳黑子数查询地址&#xff1a;点此跳转 浏览器中打开下图所示的网站。 根据自己的需求下载相应的文件查找太阳黑子数&#xff0c;下载时可以选择 TXT 文件格式&#xff0c;也可以选择 CSV 文件格式。 我下载了每日估计的太阳黑子数&#xff0c;TXT 格式文件打开如下图所示。…

免费下载的无水印人物素材网站!

在日常设计中&#xff0c;设计师常常需要使用人物素材来提升设计作品的吸引力。一个可爱的人物插画可以使网页界面更加生动&#xff0c;一个富有个性的人物素材可以让应用程序更具吸引力&#xff0c;引发用户的共鸣。但设计师有时会苦恼于找不到合适的人物素材网站&#xff0c;…

PCB制板基础知识[详细版]

一、PCB概念 PCB&#xff08;PrintedCircuitBoard&#xff09;&#xff0c;中文名称为印制电路板&#xff0c;又称印刷电路板、印刷线路板&#xff0c;是重要的电子部件&#xff0c;是电子元器件的支撑体&#xff0c;是电子元器件电气连接的提供者。由于它是采用电子印刷术制…

USB descriptor

如下为oneplus的插入MIDI模式 device descriptor 注意&#xff1a;1个device只有一个描述符 bNumConfigurations 标识只有一个配置&#xff0c;该配置只属于一个传输速度 通过getDescriptor的方法获得&#xff0c;需要指定wvalue为deivce descriptor,至于index可以为0 大概1…

【5.JS基础-JavaScript的DOM操作】

1 认识DOM和BOM 所以我们学习DOM&#xff0c;就是在学习如何通过JavaScript对文档进行操作的&#xff1b; DOM Tree的理解 DOM的学习顺序 DOM的继承关系图 2 document对象 3 节点&#xff08;Node&#xff09;之间的导航&#xff08;navigator&#xff09; 4 元素&#xff0…

Java的抽象类 接口

抽象类 如果自下而上在类的继承层次结构中上移&#xff0c;位于上层的类更具有通用性&#xff0c;甚至可能更加抽象。从某种角度看&#xff0c;祖先类更加通用&#xff0c;人们只将它作为派生其他类的基类&#xff0c;而不作为想使用的特定的实例类。例如&#xff0c;考虑一下…

python中使用opencv LED屏数字识别(可用做车牌识别,一样的原理)

应项目要求需要基于cpu的LED数字识别&#xff0c;为了满足需求&#xff0c;使用传统方法进行实验。识别传感器中显示的数字。因此使用opencv的函数做一些处理&#xff0c;实现功能需求。 首先读取图像&#xff0c;因为我没想大致得到LED屏幕的区域&#xff0c;因此将RGB转换为H…