【数据结构】AVL树相关知识详细梳理

news2024/11/17 0:06:52

1. AVL树的概念

        AVL的全称是Adelson-Velsky-Landis,其名称来源于其发明者Adelson、Velsky和Landis,

平衡二叉树搜索树

        它的出现是由于二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。这个解决办法就是AVL树。


        AVL树是具有以下性质的二叉搜索树:

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

 

        AVL树是高度平衡的二叉搜索树如果它有n个结点,其高度可保持在log_2N,搜索的时间复杂度O(log_2N)。并且克服了普通二叉搜索树可能退化,导致搜索效率大大降低的缺点。

2. AVL树原理
        

2.1 节点结构

        AVL树是通过对节点进行调整来控制树的高度以达到两端平衡,那么它是如何调整节点的呢?

        当插入节点打破了AVL树的规则----左右子树高度之差的绝对值超过1后,就会对节点进行旋转来降低子树的高度,来达到左右子树的相对平衡,避免树结构退化。

        为了方便对节点进行调整和检测,我们引入平衡因子的概念,即在每个树节点中增加一个int类型的变量来记录左右子树的高度差(这里是右子树高度 减 左子树高度),这样一来,通过分析平衡因子的大小,我们就可以判断节点是否需要旋转处理。

        显然,平衡因子的更新很多时候是牵一发而动全身的,例如:

        因此,通过平衡因子维护树结构的平衡既带来了便利,又带来了麻烦,我们往往需要从插入节点开始向上不断更新平衡因子,为了解决二叉树在向上遍历时的麻烦,我这里将节点设置为三叉,即一个节点同时拥有 父节点,左子树节点,右子树节点的指针。 

 

2.2 更新平衡因子

        在对节点进行调整前,首先要维护平衡因子,每次插入或旋转节点后,都要对相关平衡因子进行更新,旋转操作也是当发现平衡因子值异常(绝对值大于1)时才执行的。

        首先,二叉搜索树的插入节点一定是叶节点,插入节点为父节点的左子树的时候,父节点的平衡因子 -1,插入节点为父节点的右子树时,父节点的平衡因子 +1。

        然后,为了向上不断更新平衡因子,我们需要总结平衡因子更新的规律:

        1. 更新后的平衡因子为 1 或 -1(说明插入节点前,父节点的平衡因子为0),说明子树的高度变高,需要继续向上更新平衡因子。

        2. 更新后的平衡因子为 0(说明插入节点前,父节点的平衡因子为-1或1,插入节点在较矮的树那边),说明子树的高度不变,不需要再向上更新平衡因子了。

        3. 更新后的平衡因子为2 或 -2(说明插入节点前,父节点的平衡因子为-1或1,且插入节点在较高的子树那边),此时破坏了平衡规则,需要进行旋转调整。

        情况1:

       

        情况2: 

        情况3: 

 

2.3 旋转 (插入)

        二叉平衡搜索树通过旋转操作来改变节点之间的连接关系以降低数的高度,保持平衡,同时不破坏搜索树的规则。那么是如何旋转的呢?

        首先,旋转分为四种:

        1. 右单旋。

        2. 左单旋。

        3. 右左双旋。

        4. 左右双旋。

        下面通过概括图来分别描述这几种旋转对应的情况,以及如何完成旋转:

        右单旋:

        从图中可以看出,当根节点平衡因子为-2(此时左子树必定存在),且其左子树平衡因子为-1时,要进行右单旋,调整完毕后,subL和pRoot的平衡因子皆更新为0,此时子树的高度等于插入前的高度,不需要再向上更新。

        左单旋:

        类似于右单旋,当根节点平衡因子为2(此时右子树必定存在),且其左子树平衡因子为1时,要进行左单旋,调整完毕后,subL和pRoot的平衡因子皆更新为0,此时子树的高度等于插入前的高度,不需要再向上更新。 

        右左双旋:

        左右双旋:

        和右左双旋是镜像的操作,这里不再详细说明。 

        看到这里,想必你以及懂得了什么叫做旋转,也就是把下面的节点“转”到上面来,把上面的“转”下去,以到达平衡左右子树,缩小左右子树高度差的效果。

        2.4 删除

                因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,最差情况下一直要调整到根节点的位置,原理比插入更加繁琐,但也是基于旋转的原理之上的,我们只理解插入时的情况就够用了,感兴趣可以自行了解AVL树删除的实现原理。
 

3. AVL树结构模拟实现

        总体结构:

template<class T>

//AVLTree节点
struct AVLTreeNode
{
	AVLTreeNode(const T& data = T())
		: _pLeft(nullptr)
		, _pRight(nullptr)
		, _pParent(nullptr)
		, _data(data)
		, _bf(0)
	{}
	//使用三叉结构方便后续更新平衡因子
	AVLTreeNode <T>* _pLeft;//左节点指针
	AVLTreeNode <T>* _pRight;//右节点指针
	AVLTreeNode <T>* _pParent;//父节点指针
	T _data;
	int _bf; //节点的平衡因子
};

//AVL: 二叉搜索树 + 平衡因子的限制
template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
	AVLTree()
		: _pRoot(nullptr)
	{}
	//在AVL树中插入值为data的节点
	bool Insert(const T& data);
	//AVL树的验证
	bool IsAVLTree()
	{
		return _IsAVLTree(_pRoot);
	}
	//AVL树的遍历
	//void Inorder()
	//{
	//	return _Inorder(_pRoot);
	//}

private:
	//AVL树的遍历
	//void _Inorder(Node* pRoot);
	//根据AVL树的概念验证pRoot是否为有效的AVL树
	bool _IsAVLTree(Node* pRoot);
	//获取树高度
	size_t _Height(Node* pRoot);
	//右单旋
	void RotateR(Node* pParent);
	//左单旋
	void RotateL(Node* pParent);
	//右左双旋
	void RotateRL(Node* pParent);
	//左右双旋
	void RotateLR(Node* pParent);

	Node* _pRoot;//根节点
};

 获取树高:

//获取树高度
template<class T>
size_t AVLTree<T>::_Height(Node* pRoot)
{
	if (pRoot == nullptr)
		return 0;
	size_t leftsize = _Height(pRoot->_pLeft)+1;
	size_t rightsize = _Height(pRoot->_pRight)+1;
	return leftsize >= rightsize ? leftsize : rightsize;
}

插入节点:

template<class T>
bool AVLTree<T>::Insert(const T& data)
{
	//树为空,直接插入
	if (_pRoot == nullptr)
	{
		_pRoot = new Node(data);
		return true;
	}
	//根据比较规则找到插入位置
	Node* pcur = _pRoot;
	Node* parent = nullptr;
	while (pcur)
	{
		if (data == pcur->_data)
			return false;
		parent = pcur;
		if (data > pcur->_data)
			pcur = pcur->_pRight;
		else
			pcur = pcur->_pLeft;
	}
	//直接插入并更新平衡因子
	if (data > parent->_data)
	{
		pcur = new Node(data);
		parent->_pRight = pcur;
		pcur->_pParent = parent;
		parent->_bf += 1;
	}
	else
	{
		pcur = new Node(data);
		parent->_pLeft = pcur;
		pcur->_pParent = parent;
		parent->_bf -= 1;
	}
	while (parent)
	{
		// 如果插入位置子树的根平衡因子为0,则子树高度不变,不需要向上更新
		if (parent->_bf == 0)
		{
			//插入完成,返回真
			return true;
		}
		// 违反avl树规则,需要旋转
		if (parent->_bf == 2)
		{
			//右子树平衡因子为1,需要左单旋
			if (parent->_pRight->_bf == 1)
			{
				RotateL(parent);
			//旋转后子树高度不变,不需要继续向上更新
				return true;
			}

			//右子树平衡因子为-1,需要右左双旋
			else if (parent->_pRight->_bf == -1)
			{
				RotateRL(parent);
				//双旋后子树高度不变,不需要再向上更新
				return true;
			}

		}
		// 违反AVL树规则,需要旋转
		if (parent->_bf == -2)
		{
			//左子树平衡因子为-1,需要右单旋
			if (parent->_pLeft->_bf == -1)
			{
				RotateR(parent);
				//旋转后子树高度不变,不需要继续向上更新
				return true;
			}
			//左子树平衡因子为1,需要左右双旋
			else if (parent->_pLeft->_bf == 1)
			{
				RotateLR(parent);
				//双旋后子树高度不变,不需要再向上更新
				return true;
			}
		}

		// 如果插入位置子树的根平衡因子为1/-1,则子树高度增加,需要向上更新
		if (parent->_bf == 1 || parent->_bf == -1)
		{
			//如果parent不为根节点
			if (parent->_pParent)
			{
				if (parent->_data > parent->_pParent->_data)
					parent->_pParent->_bf += 1;
				else
					parent->_pParent->_bf -= 1;
			}
		}
		//向上更新
		parent = parent->_pParent;
	}
}

        实现插入代码时,重点要理清插入以及旋转的逻辑,利用节点的三叉结构,循环向上更新平衡因子并进行旋转,这也是最难的部分。 

右单旋:

//右单旋
template<class T>
void AVLTree<T>::RotateR(Node* pParent)
{
	Node* subL = pParent->_pLeft;
	Node* subLR = subL->_pRight;
	subL->_pParent = pParent->_pParent;
	//如果原pParent不为根节点
	if (pParent->_pParent)
	{
		if (subL->_data > subL->_pParent->_data)
			subL->_pParent->_pRight = subL;
		else
			subL->_pParent->_pLeft = subL;
	}
	subL->_pRight = pParent;
	pParent->_pParent = subL;
	pParent->_pLeft = subLR;
	//如果subLR不为空
	if (subLR)
		subLR->_pParent = pParent;
	//更新平衡因子
	pParent->_bf = 0;
	subL->_bf = 0;
	//若subL为根节点,更新根节点
	if (subL->_pParent == nullptr)
		_pRoot = subL;
}

左单旋:

//左单旋
template<class T>
void AVLTree<T>::RotateL(Node* pParent)
{
	Node* subR = pParent->_pRight;
	Node* subRL = subR->_pLeft;
	subR->_pParent = pParent->_pParent;
	//如果原pParent不为根节点
	if (pParent->_pParent)
	{
		if (subR->_data > subR->_pParent->_data)
			subR->_pParent->_pRight = subR;
		else
			subR->_pParent->_pLeft = subR;
	}
	subR->_pLeft = pParent;
	pParent->_pParent = subR;
	pParent->_pRight = subRL;
	//如果subRL不为空
	if (subRL)
		subRL->_pParent = pParent;
	//更新平衡因子
	pParent->_bf = 0;
	subR->_bf = 0;
	//若subL为根节点,更新根节点
	if (subR->_pParent == nullptr)
		_pRoot = subR;
}

右左双旋:

//右左双旋
template<class T>
void AVLTree<T>::RotateRL(Node* pParent)
{
	Node* subR = pParent->_pRight;
	Node* subRL = subR->_pLeft;
	//记录subRL原先的平衡因子
	int subRLbf = subRL->_bf;
	//先对subR右旋
	RotateR(subR);
	//再对pParent左旋
	RotateL(pParent);
	//更新平衡因子
	subRL->_bf = 0;
	//如果subRL原先的平衡因子为-1
	if (subRLbf == -1)
	{
		pParent->_bf = 0;
		subR->_bf = 1;
	}
	//如果subRL原先的平衡因子为1
	else if (subRLbf == 1)
	{
		pParent->_bf = -1;
		subR->_bf = 0;
	}
	//如果subRL原先的平衡因子为0
	else
	{
		pParent->_bf = 0;
		subR->_bf = 0;
	}
	//双旋后子树高度不变,不需要再向上更新
}

 左右双旋:

//左右双旋
template<class T>
void AVLTree<T>::RotateLR(Node* pParent)
{
	Node* subL = pParent->_pLeft;
	Node* subLR = subL->_pRight;
	//记录subLR原先的平衡因子
	int subLRbf = subLR->_bf;
	//先对subL左旋
	RotateL(subL);
	//再对pParent右旋
	RotateR(pParent);
	//更新平衡因子
	subLR->_bf = 0;
	//如果subRL原先的平衡因子为-1
	if (subLRbf == -1)
	{
		pParent->_bf = 1;
		subL->_bf = 0;
	}
	//如果subRL原先的平衡因子为1
	else if (subLRbf == 1)
	{
		pParent->_bf = 0;
		subL->_bf = -1;
	}
	//如果subRL原先的平衡因子为0
	else
	{
		pParent->_bf = 0;
		subL->_bf = 0;
	}
}

验证用代码: 

 最后可以搭配两个验证AVL树的代码:

//AVL树的遍历(检测结果是否有序)
/*template<class T>
void AVLTree<T>::_Inorder(Node* pRoot)
{
	if (pRoot == nullptr)
		return;
	if (pRoot->_pLeft)
		_Inorder(pRoot->_pLeft);
	cout << pRoot->_data << ' ';
	if (pRoot->_pRight)
		_Inorder(pRoot->_pRight);
}*/


//根据AVL树的概念验证pRoot是否为有效的AVL树
template<class T>
bool AVLTree<T>::_IsAVLTree(Node* pRoot)
{
    //空树为AVL树,返回true
	if(pRoot == nullptr)
		return true;
    //计算左右子树高度差
	int diff = _Height(pRoot->_pRight) - _Height(pRoot->_pLeft);
	if (diff != pRoot->_bf || (diff > 1 || diff < -1))
        //左右子树高度绝对值大于1,返回false
		return false;
	else
        //继续向下检查左右子树是否是AVL树
		return true && _IsAVLTree(pRoot->_pRight) && _IsAVLTree(pRoot->_pLeft);
}

还可以依次插入以下节点同时画图验证正确性:

        1. {16, 3, 7, 11, 9, 26, 18, 14, 15}
        2. {4, 2, 6, 1, 3, 5, 15, 7, 16, 14}

4. 总结 

理解:

        总的来说,AVLTree实现的关键在于理解旋转原理,尤其是双旋中的不同情况。

性能:

        AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2N

        但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但如果一个结构还需要经常修改,就不太适合。

 

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

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

相关文章

城市轨道交通网络客流大数据可视化分析系统----以某市交通网络客流数据为例

1 引言 1.1研究背景、目的与意义 1.1.1研究背景 城市轨道交通系统是现代城市的重要交通方式之一&#xff0c;随着城市化进程的加速和人口增长&#xff0c;轨道交通系统的客流量不断增加。因此&#xff0c;轨道交通部门和相关企业需要对客流数据进行实时监测和分析&#xff0…

云数据库RDS MySQL性能测试与对比@2024年09月

原创&#xff1a;orczhouninedata 来源&#xff1a;云数据库技术 在不同的云厂商购买相同规格的MySQL实例(如4vCPU-16GB)&#xff0c;获得的性能相同吗&#xff0c;他们的差异如何&#xff1f;本文继续尝试回答这个问题。 详细数据&#xff1a; 测试结果概述 在本次测试中&…

常见的TTL,RS232,RS485,IIC,SPI,UART之间的联系和区别

简单总结 图片来源 RS232,RS485可参考&#xff0c;IIC&#xff0c;SPI,UART可参考 烧录程序中常听到的一句话就是USB转TTL&#xff0c;但严格来说算是USB传输数据的协议转换成TTL&#xff08;Transistor-Transistor Logic&#xff09;协议传输数据。首先&#xff0c;usb是常见…

电脑资料被拷贝了,能查出来吗?5个方法有效防止电脑泄密!

网络快速发展的背景下&#xff0c;电脑资料的安全问题日益凸显。 一旦电脑资料被非法拷贝&#xff0c;不仅可能导致企业核心机密泄露&#xff0c;还可能对个人隐私造成严重影响。 那么&#xff0c;当电脑资料被拷贝时&#xff0c;我们能否查出来&#xff1f;又该如何有效防止…

【Python】必学!教你如何在日志中隐藏明文密码?看完包会的!(附带免费源码)

前言 在项目开发中&#xff0c;有的时候会遇到一些安全需求&#xff0c;用以提升程序整体的安全性&#xff0c;提高外来非法攻击的门槛&#xff0c;而在日志中隐藏明文密码打印便是最典型的安全需求之一。 在Python中&#xff0c;明文密码往往发生于命令执行参数、debug日志、…

施耐德EcoStruxure Machine SCADA Expert(EMSE)数据监测(十八)

通过EMSE与sql数据库连接,可以实现一些过程数据的监测、存档,实现生产过程的可视化。 1.创建sql数据库表单 新建一个名为Table_Monitor的表单,添加三个元素:Re_Index 序号;Re_Date 时间;Re_Temper 温度(需要监测的数据) 2.EMSE内关联变量 2.1 先创建网格 2.2 选择数据…

unity CustomEditor的基本使用

CustomEditor用来自定义脚本的编辑面板 其基本使用方式 先准备一个类&#xff0c;继承MonoBehaviour 定义一个变量&#xff0c;然后准备一个类&#xff0c;继承自Editor 在CustomEditor中指定要去修改的类型&#xff0c;通过serializedObject.FindProperty(变量名)的方式来获…

Ubuntu下安装向日葵:闪退

下载 https://sunlogin.oray.com/download 初次安装 $ sudo dpkg -i SunloginClient_15.2.0.63064_amd64.deb 正在选中未选择的软件包 sunloginclient。 (正在读取数据库 ... 系统当前共安装有 234281 个文件和目录。) 准备解压 SunloginClient_15.2.0.63064_amd64.deb ..…

Java.动态代理

1.创建一个接口 package Mydynamicproxy1;public interface Star {public abstract String sing(String str);public abstract void dance(String str); }2.创建一个BigStar类&#xff0c;要实现Star这个接口 package Mydynamicproxy1;public class BigStar implements Star{…

甘肃非遗文化网站:Spring Boot开发实战

3 系统分析 当用户确定开发一款程序时&#xff0c;是需要遵循下面的顺序进行工作&#xff0c;概括为&#xff1a;系统分析–>系统设计–>系统开发–>系统测试&#xff0c;无论这个过程是否有变更或者迭代&#xff0c;都是按照这样的顺序开展工作的。系统分析就是分析系…

Java EE中的编码问题及解决方案

Java EE中的编码问题及解决方案 在Java EE开发中&#xff0c;处理字符编码是确保数据正确传输和显示的重要环节。不同的编码不一致会导致乱码&#xff0c;影响用户体验。本文将总结在Java EE中可能遇到的编码问题及其解决方案。 1. 输入数据编码问题 在表单提交时&#xff0c…

【中级通信工程师】终端与业务(三):电信业务

【零基础3天通关中级通信工程师】 终端与业务(三)&#xff1a;电信业务 本文是中级通信工程师考试《终端与业务》科目第三章《电信业务》的复习资料和真题汇总。终端与业务是通信考试里最简单的科目&#xff0c;有效复习通过率可达90%以上&#xff0c;本文结合了高频考点和近几…

代码随想录算法训练营第十六天|512.找树左下角的值 112. 路径总和 113. 路径总和ii 106.从中序与后序遍历序列构造二叉树

512.找树左下角的值 给定一个二叉树&#xff0c;在树的最后一行找到最左边的值。 示例 1: 示例 2: 思路&#xff1a; 递归三部曲&#xff1a; 参数和返回值&#xff1a;传入节点是参数&#xff0c;返回值是最终值int终止条件&#xff1a;遇到空节点直接返回&#xff0c;或者…

SD2.0 Specification之写保护

文章目录 1 机械开关写保护&#xff08;由主机负责实现效果&#xff09;2 卡内部写保护&#xff08;由卡负责实现&#xff09;3 密码写保护 本文章主要讲解关于SD2.0写保护功能的内容&#xff0c;基础概念和其它内容请参考以下文章。 SD2.0 Specification简述 SD卡支持3种写保护…

论文阅读《Co-clustering for Federated Recommender System》

论文概况 本文是2024 WWW的一篇联邦推荐论文&#xff0c;提出了一个基于特定类别物品相似度来进行聚类的联邦推荐框架。 Introduction 分析了经典聚类技术KMeans在联邦推荐设置中的不足&#xff0c;提出了一种新的共聚类联邦推荐机制CoFedRec&#xff0c;该机制在每个通信回合…

华为NAT ALG技术的实现

双向NAT技术&#xff1a;经过防火墙的2报文源IP地址和目的IP地址都同时被转换&#xff0c;外网发送报文给内网服务器&#xff0c;先转换目的IP地址&#xff0c;然后符合安全策略后&#xff0c;在替换源IP地址&#xff0c;然后将记录写入防火墙会话表&#xff0c;并发送出报文&a…

结构化知识抽取案例

假设我们有一个包含中文电影信息的数据库表 movies&#xff0c;其中包含以下字段&#xff1a; movie_id (电影ID)title (电影标题)year (上映年份)genre (类型)director (导演)rating (评分) 表中的部分数据如下&#xff1a; 知识抽取步骤 数据获取&#xff1a;从数据库中查…

WAF,全称Web Application Firewall,好用WAF推荐

WAF&#xff0c;全称Web Application Firewall&#xff0c;即Web应用防火墙&#xff0c;是一种网络安全设备&#xff0c;旨在保护Web应用程序免受各种Web攻击&#xff0c;如SQL注入、跨站脚本&#xff08;XSS&#xff09;、跨站请求伪造&#xff08;CSRF&#xff09;等。 WAF通…

第166天:应急响应-拒绝服务钓鱼指南DDOS压力测试邮件反制分析应用日志

案例一&#xff1a;内网应急-日志分析-爆破&横向&数据库 数据库 这里不同数据库日志不一样&#xff0c;我用mysql分析 首先MySQL数据库需要支持远程连接 GRANT ALL PRIVILEGES ON . TO root% IDENTIFIED BY 123.com WITH GRANT OPTION; 其次开启日志 -- 查看general…

Qt_线程介绍与使用

目录 1、QThread常用API 2、Qt线程安全 3、使用线程QThread 4、connect函数的第五个参数 5、Qt互斥锁 5.1 QMutexLocker 6、条件变量 7、信号量 结语 前言&#xff1a; 线程是应用程序开发非常重要的概念&#xff0c;在Qt中&#xff0c;用QThread类来实现多线程&a…