一篇搞定AVL树+旋转【附图详解旋转思想】

news2024/11/27 4:41:30

🎉个人名片:

🐼作者简介:一名乐于分享在学习道路上收获的大二在校生
🙈个人主页🎉:GOTXX
🐼个人WeChat:ILXOXVJE
🐼本文由GOTXX原创,首发CSDN🎉🎉🎉
🐵系列专栏:零基础学习C语言----- 数据结构的学习之路----C++的学习之路
🐓每日一句:如果没有特别幸运,那就请特别努力!🎉🎉🎉
————————————————

一.AVL 树

1.1 AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。(上章提过)
所以就有人发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

AVL树的条件:
一棵AVL树可以是空树,是具有以下性质的二叉搜索树:
1.它的左右子树都是AVL树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1 (-1/0/1)(如下图)(这里讨论的该节点的平衡因子=右子树高度-左子树高度

注意: 下图的平衡因子=左子树高度-右子树高度
在这里插入图片描述
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。

1.2 AVL树节点的定义

                                //树节点的定义
template<class K,class V>
struct AVLNode
{ 
	AVLNode<K, V>* _left;        //存储左节点
	AVLNode<K, V>* _right;       //存储右节点
	AVLNode<K, V>* _parent;      //存储父亲
	pair<K,V> _kv;               //存储数据
	int _bl;                     //平衡因子

	AVLNode(pair<K, V>& kv)      //构造函数
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bl(0)     
	{}
};

1.3 AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子

平衡因子的处理方法:

Cur插入后,Parent的平衡因子一定需要调整,在插入之前,Parent 的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:

  1. 如果Cur插入到Parent的左侧,只需给Parent的平衡因子-1即可
  2. 如果Cur插入到Parent的右侧,只需给Parent的平衡因子+1即可

此时:Parent的平衡因子可能有三种情况:0,正负1, 正负2

  1. 如果Parent的平衡因子为0,说明插入之前Parent的平衡因子为正负1,插入后被调整成0,此时满足 AVL树的性质,插入成功。
  2. 如果Parent的平衡因子为正负1,说明插入前Parent的平衡因子一定为0,插入后被更新成正负1,此时以Parent为根的树的高度增加,需要继续向上更新。
  3. 如果Parent的平衡因子为正负2,则Parent的平衡因子违反平衡树的性质,需要对其进行旋转处理。

【旋转代码在下面讲解后】
实现代码:

bool insert(pair<K,V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
	}

	//找插入位置
	Node* parent = nullptr;   //记录插入位置的父亲,方便插入后链接
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv > kv)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_kv < kv)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}
	//到这里就说明找到插入位置了,下面就开始插入了
	//判断插在父亲的左边还是右边
	if (cur == parent->left)             
	{
		cur = new Node(kv);      
		parent->_left = cur;
		cur->_parent = parent;
		cur->_parent->_bl--;       //如果是在左边插入,则--平衡因子
	}
	else if (cur == parent->right)
	{
		cur = new Node(kv);
		parent->_right = cur;
		cur->_parent = parent;
		cur->_parent->_bl++;      //如果是在右边插入,则++平衡因子
	}
	//调节上面节点的平衡值
	while (parent)
	{
		//情况1:插入节点后父亲的平衡因子改变
		if (parent->_bl == 1 || parent->_bl == -1)
		{
			Node* grandfather = parent->_parent;
			if (parent = grandfather->_left)
			{
				grandfather->_bl--;
				parent = grandfather;
			}
			else
			{
				grandfather->_bl++;
				parent = grandfather;
			}
		}
		//情况二:插入节点后父亲的平衡因子不改变
		else if (parent->_bl == 0)
		{
			break;
		}
		//情况三:父亲的平衡因子已经不满足AVL树的条件
		//需要旋转处理
		else if ()
		{
		/下面的旋转代码后面讲解后贴出
			//左单旋
			if ()
			{
				
			}
			//右单旋
			else if ()
			{
				
			}
			//左右双旋
			else if ()
			{
			}
			//右左双旋
			else if ()
			{
			}
		}
	}
}

1.4AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
单旋分为:左单旋与右单旋
双旋分为:左右双旋与右左双旋

单旋思想

如图
在这里插入图片描述
如上图,上面的左右单旋,要么是在parent的左子树高,并且左子树中左边高(左左高)
要么是parent的右子树高,并且右子树中右边高(右右高),这种及只需要旋转一边就可以解决不平衡的问题,哪边高,就往另一边旋转即可。

1.4.1左单旋

使用场景:
新节点插入较高右子树的右侧—右右:左单旋。

如图:长方形代表高度为h的子树。

在这里插入图片描述
具体例子:
在这里插入图片描述

解析:

含义解析:

pNodeR = parent->_right
pNodeRL = pNodeR->_left

思想解析:
如上图,右单旋是让pNodeRL节点成为parent的右孩子,然后parent自己变为pNodeR的左孩子,pNodeR变成这个子树的根。

平衡因子的调节:
单旋后pNodeR与parent的平衡因子都变为0;

注意:
在旋转过程中,有以下几种情况需要考虑:

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

实现代码

//左单旋
void rotateL(Node* parent)
{
	Node* pparent = parent->_parent;  //记录所旋转根节点的父亲
	Node* pNodeR = parent->_right;   
	Node* pNodeRL = pNodeR->_left;
	if (pNodeRL)                //如果该旋转节点的右节点的左孩子存在
		parent->_right = pNodeRL;
	pNodeR->_left = parent;  

	//新的父节点的链接
	if (parent == _root)       //若parent是根节点
	{
		_root = pNodeR;
		pparent = nullptr;     
	}
	else                     //parent不是根节点
	{
		if (pparent->_left == parent)
		{
			pparent->_left = pNodeR;
		}
		else
		{
			pparent->_right = pNodeR;
		}
	}
	pNodeR->_bl = 0;
	parent->_bl = 0;
}
1.4.2右单旋

使用场景:
新节点插入较高左子树的左侧—左左:右单旋

如图
在这里插入图片描述
具体例子:
在这里插入图片描述

解析:

含义解析

pNodeL = parent->_Left
pNodeLR = pNodeL->_right

思想解析

如上图,右单旋是让pNodeLR节点变为parent的左孩子,然后parent自己变为pNodeL的右孩子,pNodeL变成这个子树的根。

平衡因子的调节
单旋后pNodeL与parent的平衡因子都变为0;

在旋转过程中,有以下几种情况需要考虑:

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

代码实现

/右单旋
	void rotateR(Node* parent)
	{
		Node* pparent = parent->_parent;

		Node* pNodeL = parent->_left;
		Node* pNodeLR = pNodeL->_right;

		if (pNodeLR)
			parent->_left = pNodeLR;
		pNodeL->_right = parent;

		if (parent == _root)
		{
			_root = pNodeL;
			pparent = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
			{
				pparent->_left = pNodeL;
			}
			else
			{
				pparent->_right = pNodeL;
			}
		}
		pNodeL->_bl = 0;
		parent->_bl = 0;
	}
双旋思想

如图
在这里插入图片描述
如上图,如果parent的左子树高,并且左子树中的右子树高(左右高),或则是parent的右子树高,并且右子树的左子树高(右左高),则旋转一次不能解决问题,所以就有了双旋的思想。

1.4.3左右单双旋

使用场景:

新节点插入较高左子树的右侧—左右:先左单旋再右单旋

如图
在这里插入图片描述
具体例子:
在这里插入图片描述
解析:
因为是parent的左子树中的右子树高,所以只需要先将parent的左子树进行左旋,将parent的左子树变为左边高,则旋转后parent整个树就变为了左左高,再用上面单旋的思想,parent以旋转点进行右旋即可;

else if (parent->_bl == -2 && parent->_left->_bl == 1)
{
				int bl = parent->_left->_right->_bl;
				Node* pNodeL = parent->_left;
				Node* pNodeLR = pNodeL->_right;

				rotateL(pNodeL);     //左旋转
				rotateR(parent);    //右旋转

				if (-1 == bl)      //分情况调节平衡因子
				{
					pNodeLR->_bl = 0;
					pNodeL->_bl = 0;
					parent->_bl = 1;
				}
				else if (1 == bl)
				{
					pNodeLR->_bl = 0;
					parent->_bl = 0;
					pNodeL->_bl = -1;
				}
				else
				{
					pNodeLR->_bl = 0;
					parent->_bl = 0;
					pNodeL->_bl = 0;
				}
}
1.4.4右左单旋

使用场景:
新节点插入较高右子树的左侧—右左:先右单旋再左单旋

如图
在这里插入图片描述
具体例子:
在这里插入图片描述

解析:

因为是parent的右子树中的左子树高,所以只需要先将parent的右子树进行右旋,将parent的右子树变为右边高,则旋转后parent整个树就变为了右右高,再用上面单旋的思想,parent以旋转点进行左旋即可;

代码实现

else if (parent->_bl == 2 && parent->_right->_bl == -1)
{
				Node* pNodeR = parent->_right;
				Node* pNodeRL = pNodeR->_left;
				int bl = pNodeRL->_bl;

				rotateR(pNodeR);   //先右旋
				rotateL(parent);   //再左旋

				pNodeRL->_bl = 0;    //分情况调节平衡因子
				if (1 == bl)
				{
					parent->_bl = -1;
					pNodeR->_bl = 0;
				}
				else if (-1 == bl)
				{
					parent->_bl = 0;
					pNodeR->_bl = 1;
				}
				else
				{
					parent->_bl = 0;
					pNodeR = 0;
				}
}
双旋后平衡因子的调节

我们从结果来看,忽略过程,从图中可以得到
在这里插入图片描述
解析:
实际上就是将60的左孩子给了30的右孩子,把60的有孩子给了90的左孩子。
所以可以得出:
平衡因子的改变与60的平衡因子有关(与它的左右孩子有关)。
情况分为3种:60的平衡因子为(1,0,-1)

下图解为:右左双旋
当为1时:
在这里插入图片描述
结论:

pNodeRL的平衡因子为0
parent–>-1
pNodeR–>0

当为0时:
在这里插入图片描述
结论:

pNodeRL的平衡因子为0
parent–>0
pNodeR–>0

当为-1时:
在这里插入图片描述
结论:

pNodeRL的平衡因子为0
parent–>0
pNodeR–>1

左右双旋的图解
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

旋转总结:

假如以Parent为根的子树不平衡,即Parent的平衡因子为2或者-2
分以下情况考虑:

  1. Parent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pNodeR 当pNodeR的平衡因子为1时,执行左单旋 当pNodeR的平衡因子为-1时,执行右左双旋
  2. Parent的平衡因子为-2,说明Parent的左子树高,设Parent的左子树的根为pNodeL 当pNodeL的平衡因子为-1是,执行右单旋 当pNodeL的平衡因子为1时,执行左右双旋
    旋转完成后,原Parent为根的子树个高度降低,已经平衡,不需要再向上更新。

1.5 AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    节点的平衡因子是否计算正确。

代码实现
方法一:

	int Hight(Node* root)    //计算该节点的高度
	{
		if (root == nullptr)
		{
			return 0;
		}
		int Hightleft = Hight(root->_left);
		int Hightright = Hight(root->right);

		return Hightleft > Hightright ? Hightleft + 1 : Hightright + 1;  //返回左右子树高的那一个
	}

	bool _isbalance(Node* root)
	{
		if(root==nullptr)
		{
			return true;
		}
		int hightleft = Hight(root->_left);    //计算左右子树的高度
		int hightright = Hight(root->_right);

		if (abs(hightright - hightleft) >= 2)   //判断高度差
		{
			return flase;
		}
		if (hightright - hightleft !=root->_bl)    //判断计算结果是否与该节点的平衡因子相等
		{
			cout << root->_kv->first<<':' << "异常" << endl;
			return false;
		}

		return isbalance(root->_left) && isblance(root->_right);  //递归
	}

方法一有大量的重复计算(每一个节点都需要重新计算高度)
方法二更优

方法二:

	bool _isbalance(Node* root,int& height)   //height记录高度
	{
		if (root == nullptr)
		{
			height = 0;
			return true;
		}
		if (!isbalance(root->_left,height) || !isblance(root->_right,height))
		{
			return false;
		}
		int heightleft = 0;
		int heightright = 0;

		if (abs(heightright - heightleft) >= 2)    //如果高度差超过1,则不平衡,返回false
		{
			return false;
		}
		if (heightright - heightleft != root->_bl)   //检查该节点的平衡因子是否正确
		{
			cout << root->_kv->first << ':' << "异常" << endl;
			return false;
		}
		height = heightleft > heightright ? heightleft + 1 : heightright + 1;   //计算height的值

		return true;
	}
	bool isbalance()
	{
		return _isbalance(_root);
	}

1.6 AVL树的性能分析

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

本章完~

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

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

相关文章

java AIO为什么用的并不多

Java AIO的本质是什么 原文&#xff1a;https://blog.csdn.net/hellojackjiang2011/article/details/131322757?spm1001.2101.3001.6650.1&utm_mediumdistribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-131322757-blog-103915337.235%5Ev43%5Epc_blo…

鸿蒙OS(ArkTS) 案例:【使用http网络请求框架加载验证码】

需求&#xff1a;加载验证码&#xff1b;1.下载验证码图像文件&#xff1b;2.获取header里面验证码ID 踩坑--踩坑--踩坑 根据文档使用 request.downloadFile 请求&#xff0c;官方示例: // pages/xxx.ets // 将网络资源文件下载到应用文件目录并读取一段内容 import common …

微信小程序开发【从入门到精通】——页面导航

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

【面试专题】JVM相关

1.为什么需要JVM&#xff0c;不要JVM可以吗&#xff1f; 1.JVM可以帮助我们屏蔽底层的操作系统 一次编译&#xff0c;到处运行 2.JVM可以运行Class文件 2.JDK&#xff0c;JRE以及JVM的关系 3.我们的编译器到底干了什么事&#xff1f; 仅仅是将我们的 .java 文件转换成了 .cl…

帆软报表在arm架构的linux

有朋友遇到一个问题在部署帆软报表时遇到报错。 问 我在 arm架构的linux服务器上部署帆软报表遇到了一个棘手的问题&#xff0c;你有空帮忙看下嘛。 我看后台日志报的错是 需要升级 gcc、libmawt.so &#xff0c;是系统中缺少Tomcat需要的依赖库&#xff0c;你之前处理过类似…

基于uQRCode封装的前端二维码生成组件实践

在前端开发中&#xff0c;二维码生成已成为一种常见需求。二维码凭借其简洁、方便的特点&#xff0c;被广泛应用于产品推广、信息交互等多个场景。在此背景下&#xff0c;开发一个易于使用且性能优越的二维码生成组件变得至关重要。本文基于uQRCode封装了一个前端二维码生成组件…

详解JAVA程序调优

目录 1.概述 2.命令 2.1.查看JAVA进程 2.2.查看虚拟机状态 2.3.查看线程的情况 3.工具 3.1.jconsole 3.2.jvisualVM 4.实战场景 1.概述 在实际工作中我们难免会遇见程序执行慢、线程死锁等一系列的问题&#xff0c;这时候就需要我们定位具体问题然后来解决问题了。所…

安科瑞路灯安全用电云平台解决方案【电不起火、电不伤人】

背景介绍 近年来 &#xff0c;随着城市规模的不断扩大 &#xff0c;路灯事业蓬勃发展。但有的地方因为观念、技术、管理等方面不完善 &#xff0c;由此引发了一系列安全问题。路灯点多面广 &#xff0c;一旦漏电就极容易造成严重的人身安全事故。不仅给受害者家庭带来痛苦 &am…

抽象类和接口的简单认识

目录 一、抽象类 1.什么是抽象类 2.抽象类的注意事项 3.抽象类与普通类的对比 二、接口 1.接口的简单使用 2.接口的特性 3.接口的使用案例 4.接口和抽象类的异同 一、抽象类 所谓抽象类&#xff0c;就是更加抽象的类&#xff0c;也就是说&#xff0c;这个类不能具体描…

雷卯有多种接口与电源保护方案

在当今的电子设备中&#xff0c;各种接口和电源保护至关重要。它们不仅关乎设备的正常运行&#xff0c;更直接影响到数据传输的稳定性和设备的安全。雷卯公司以其专业的技术和丰富的经验&#xff0c;为您提供全面的接口与电源保护方案&#xff0c;确保您的系统安全稳定运行。 …

图像分割论文阅读:Automatic Polyp Segmentation via Multi-scale Subtraction Network

这篇论文的主要内容是介绍了一种名为多尺度差值网络&#xff08;MSNet&#xff09;的自动息肉分割方法。 1&#xff0c;模型整体结构 整体结构包括编码器&#xff0c;解码器&#xff0c;编码器和解码器之间是多尺度差值模块模块&#xff08;MSM&#xff09;&#xff0c;以及一…

golang grpc和protobuf的版本降级问题(version4 -> version3)

最后更新于2024年3月28日 10:57:52 简中没查到类似的文章。一点小事闹麻了&#xff0c;搞了一天&#xff0c;特意发出来造福大家。 所谓的版本就是下面这个东西proto.ProtoPackageIsVersion4或者proto.ProtoPackageIsVersion3&#xff1a; 目的 为了适配旧代码&#xff0c…

探索c++:string常用接口 迷雾

个人主页&#xff1a;日刷百题 系列专栏&#xff1a;〖C/C小游戏〗〖Linux〗〖数据结构〗 〖C语言〗 &#x1f30e;欢迎各位→点赞&#x1f44d;收藏⭐️留言&#x1f4dd; ​ ​ 一、string类 这里我们对string类进行一个简单的总结&#xff1a; string是表示字符串的字…

蓝桥小白入门赛6

原题链接&#xff1a;第 6 场 小白入门赛 - 蓝桥云课 目录 A、元宵节快乐 B、猜灯谜 C、数学奇才 D、你不干&#xff1f;有的是帕鲁干&#xff01; E、等腰三角形 F、 计算方程 A、元宵节快乐 签到题 print("Today AK!") B、猜灯谜 模拟&#xff0c;特判下…

容器四(Map 接口)

目录 HashMap 和 HashTable Map 接口中的常用方法 HashMap 底层实现 Hashmap 基本结构 存储数据过程 put(key,value) 取数据过程 get(key) 扩容问题 JDK8 将链表在大于 8 情况下变为红黑二叉树 Map 就是用来存储“键(key)&#xff0d;值(value) 对”的。 Map 类中存储的…

《科技创新与应用》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答&#xff1a; 问&#xff1a;《科技创新与应用》是什么级别期刊&#xff1f; 答&#xff1a;省级&#xff1b;主管单位&#xff1a;黑龙江省科学技术协会&#xff1b;主办单位&#xff1a;黑龙江省创联文化传媒有限公司 问&#xff1a;《科技创新与应用》是核心期刊…

链游系统开发运营版丨链游系统开发指南教程

在当今数字经济时代&#xff0c;区块链技术的发展不仅改变了金融行业&#xff0c;也深刻影响了游戏产业。链游系统&#xff08;Blockchain Game System&#xff09;作为区块链技术与游戏行业的结合&#xff0c;正在成为新一代游戏的趋势。本文将为您详细介绍链游系统的开发与运…

2024年【N1叉车司机】考试技巧及N1叉车司机复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 N1叉车司机考试技巧参考答案及N1叉车司机考试试题解析是安全生产模拟考试一点通题库老师及N1叉车司机操作证已考过的学员汇总&#xff0c;相对有效帮助N1叉车司机复审考试学员顺利通过考试。 1、【多选题】《中华人民…

南京观海微电子---Vitis HLS的工作机制——Vitis HLS教程

1. 前言 Vitis HLS&#xff08;原VivadoHLS&#xff09;是一个高级综合工具。用户可以通过该工具直接将C、 C编写的函数翻译成HDL硬件描述语言&#xff0c;最终再映射成FPGA内部的LUT、DSP资源以及RAM资源等。 用户通过Vitis HLS&#xff0c;使用C/C代码来开发RTL IP核&#x…

思通数科:利用开源AI能力引擎平台打造企业智能搜索系统

在信息爆炸的时代&#xff0c;如何高效地管理和检索海量数据已成为企业和个人面临的一大挑战。思通数科 StoneDT 多模态AI能力引擎平台&#xff0c;以其强大的自然语言处理&#xff08;NLP&#xff09;、OCR识别、图像识别和文本抽取技术&#xff0c;为用户带来了前所未有的智能…