二叉树进阶--二叉搜索树的进一步优化--AVL树 Self-balancing binary search tree

前言:

在上一次的文章中,我们详细介绍了二叉树的进阶树型,即BS树(二叉搜索树),但在文章的结尾,二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,我们迫切需要一个可以解决这个问题同时又不会破坏BS树本身性质的解决方案,这便是我们今天的AVL树。

AVL树的性质:

前言问题的解决方法:

对于前言中的问题,AVL树的解决方案为:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度
这样就避免了出现单支树的情况,同时保证了BS树的结构不变。

AVL树的性质:

故我们现在就可以总结出AVL树的一般规律了:
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1.它的左右子树都是AVL树,同时符合BS树的基本规律
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)。
如图:
在这里插入图片描述
对于这样一棵单支树,我们就可以变成:
在这里插入图片描述
这样就提高了我们的搜索效率!
因此,我们也可以把AVL树简记为:一棵进行了高度平衡的二叉搜索树

!!!AVL树的实现(重点):

对于AVL树的结构和性质我们已经足够了解了,现在就让我们去实现一下AVL树吧:

1.单个节点结构体:

对于AVL树的每一个节点来说,首先我们必备的就是数据,左指针,右指针。其次,由于我们在平衡树的过程中涉及到一个频繁的改变节点指向的问题,因此我们还需要一个指向上一个父亲节点的指针parent,同时,我们还需要对每个节点存储一个平衡因子,以方便对树进行平衡。
因此,我们可以这样构建结构体:

template<class K,class V>
struct AVLTreeNode//构建三叉链,多加一个指向上一个节点的指针_parent
{
	AVLTreeNode(const pair<K, V>& kv)//构造函数
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;//更新平衡因子,父亲指针,指向节点的父亲
	pair<K, V> _kv;//kv键值对
	int _bf;//平衡因子,balance factor
};

由于是为了后续的map和set做准备,因此我们这里采取使用键值对pair的方式来当作本次我们的数据的数据类型_kv
注意,别忘了构造函数,这里不能用默认的,因为bf我们是都要从0开始初始化的。

2.AVL树类主体:

对于AVL树而言,我们的主类形式大体如下:

template<class K, class V>
class AVLTree
{
	typedef struct AVLTreeNode<K, V> Node;
public:
  //在这里实现对应的函数功能
  //
  //
  //............
private:
	Node* _root = nullptr;
}

和二叉树的基本结构一样,我们的成员同样也只需要一个root根节点即可,方便遍历和向下移动等操作。

3.AVL树的插入!!!:

AVL树的插入,就是我们AVL树的精华所在,我们也是在这里进行平衡的,在这里,我大致将插入分为两个部分
1.常规的BS树插入过程
2.平衡树的过程

1.树的插入过程:

AVL树由于其本质和BS树差不多,因此前面的插入过程也大体相似,其步骤逻辑就是:
首先找到对应的位置,然后创建节点在对应的位置插入,这两部分。需要注意的地方是:我们要对空树进行判断,如果是空树就直接root=对应的新节点即可了,其他的同BS树的插入步骤一样。
代码如下:

bool insert(const pair<K,V>& kv)
{
	//插入过程,与搜索二叉树一样
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}//指针移动完毕,找到我们要插入节点的对应空位,直到nullptr为止,然后开始插入节点
		cur = new Node(kv);
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
			cur->_parent = parent;//让父亲指针指向插入节点的父亲节点
		}
		else if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
    
    //下面就是第二部分平衡树的过程
    //..............
}

2.树的平衡过程:

当我们插入完节点后,倘若不平衡,那它就是一棵普通的二叉搜索树无疑,因此只有通过接下来的平衡过程,它才会变成一棵我们看到的AVL树。
在这里,我大致将其分为两个部分:
1.调控平衡因子
2.通过平衡因子对树进行调整
我们大体的代码结构如下:
首先我们要说明平衡因子数值的计算方式:
以节点右树的高度为正,节点左树高度为负,通过右减去左来确定对应的平衡因子的数值。

while(parent)//和向上调整一样,看parent的改变效果,直到达到根节点为止,此时cur为根节点,parent为空了
{
	if (cur == parent->_left)
	{
		parent->_bf--;//对父亲平衡因子进行调整
	}
	else if (cur == parent->_right)
	{
		parent->_bf++;
	}
    //首先是先对我们第一部分创建的parent的平衡因子进行更新,从parent开始持续性地向上检查调整平衡因子,从而做出对应的平衡策略
	if (parent->_bf == 0)
	{
		break;
	}//如果平衡因子为0,说明两边高度差相同,这说明这棵树是平衡的,那就不需要向上调整了,直接结束本次插入即可
	else if(parent->_bf==1||parent->_bf==-1)
	{
		cur = parent;
		parent = parent->_parent;//这里存储的parent,就是为了向上调整因子而准备的
	}//如果为1/-1,说明存在平衡不平衡的可能性,则继续向上调整
	else if(parent->_bf==2||parent->_bf==-2)//2或者-2的情况,需要旋转平衡调整
	{
		//这里是针对不同情况进行旋转调整的过程
	}
	else//这里说明出现大于2的情况了,已经不是AVL树了,不调整直接报错,我们规定平衡因子的绝对值超过2以上就已经不是AVL树了,直接报错即可
	{
		assert(false);
	}
}

3.针对不同情况对树的调整方法:

我们在调控AVL树的时候,会遇到下面的四种情况:
1… 新节点插入较高左子树的左侧—(简记为左左):
如图:
在这里插入图片描述
而我们的目的是将其调整为下面的情况:
在这里插入图片描述
故在这里,我们就采取右单旋的方法即可解决,方法思路如下:
在这里插入图片描述
对于方框的部分,我们不需要去管,只需要对parent,sub1,sub2进行处理即可,最后别忘了对平衡因子进行更新处理即可。
代码如下:

void RotateR(Node* parent)//右单旋旋转调整
{
	Node* sub1 = parent->_left;
	Node* sub2 = sub1->_right;
	Node* PPparent = parent->_parent;
	parent->_left = sub2;
	if (sub2)
	{
		sub2->_parent = parent;
	}
	sub1->_right = parent;
	parent->_parent = sub1;
	if (_root == parent)//如果为根节点,直接更新root即可,父亲指针指向空
	{
		_root = sub1;
		sub1->_parent = nullptr;
	}
	else//如果不为,则依照正常的逻辑进行父亲节点和对应的root节点进行互相指向即可
	{
		if (PPparent->_right == parent)
		{
			PPparent->_right = sub1;
		}
		else if (PPparent->_left == parent)
		{
			PPparent->_left = sub1;
		}
		sub1->_parent = PPparent;
	}
	parent->_bf = sub1->_bf = 0;
}

在代码这里我们需要注意几个点:
1.sub2是有可能存在为空的情况的,因此我们需要对sub2进行判断,否则很可能会出现空指针访问的情况
2.别忘了平衡因子
3.别忘了对新调整的sub1的parent进行处理,要让其指向的父亲节点和它对应好关系

2.新节点插入较高右子树的右侧—右右:左单旋
在这里插入图片描述
由于其本质和右单旋属于镜面对称的方式,因此我这里直接上代码,其需要注意的点和左单旋一样,我这里直接上代码了:

void RotateL(Node* parent)//左单旋旋转调整
{
	Node* sub1 = parent->_right;
	Node* sub2 = sub1->_left;
	Node* PPparent = parent->_parent;
	parent->_right = sub2;
	if (sub2)//注意这里,可能涉及到sub2为空的情况,比如h==0,此时要对sub2是否为空进行一次判断
	{
		sub2->_parent = parent;
	}
	sub1->_left = parent;
	parent->_parent = sub1;
	if (_root == parent)
	{
		_root = sub1;
		sub1->_parent = nullptr;
	}
	else
	{
		if (PPparent->_right == parent)
		{
			PPparent->_right = sub1;
		}
		else if (PPparent->_left == parent)
		{
			PPparent->_left = sub1;
		}
		sub1->_parent = PPparent;
	}
	parent->_bf = sub1->_bf = 0;
}

我们大致的调控方法如下:
在这里插入图片描述
3.新节点插入较高左子树的右侧—左右:先左单旋再右单旋
这种属于情况比较复杂的情况,大致的情况图示如下:
在这里插入图片描述
我们处理方式为:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。
代码如下:

	void RotateLR(Node* parent)//左右双旋转
	{
		Node* sub1 = parent->_left;
		Node* sub2 = sub1->_right;
		int bf = sub2->_bf;
		RotateL(parent->_left);
		RotateR(parent);
		if (bf == 0)
		{
			parent->_bf = sub1->_bf = sub2->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			sub2->_bf = 0;
			sub1->_bf = -1;
		}
		else if (bf == -1)
		{
			sub2->_bf = 0;
			parent->_bf = 1;
			sub1->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

这种方法需要注意的就是,我们需要先存储sub2,即图中数值为60的节点的平衡因子数,即为代码中的bf,因此我们从图中可以得知,sub2的左右分别给了parent和sub1,因此我们就可以通过知道sub2左右的节点情况,对最后的parent sub1 sub2三个节点的平衡因子进行调整。

4.新节点插入较高右子树的左侧—右左:先右单旋再左单旋
在这里插入图片描述
代码如下:

void RotateRL(Node* parent)//右左双旋转
{
	Node* sub1 = parent->_right;
	Node* sub2 = sub1->_left;
	int bf = sub2->_bf;
	RotateR(parent->_right);
	RotateL(parent);
	if (bf == 0)
	{//即插入的点就是分解后的sub2,此时对应的平衡树两边层数相同
		parent->_bf = sub1->_bf = sub2->_bf = 0;
	}
	else if (bf == 1)
	{
		//sub2右子树新增
		sub2->_bf = 0;
		parent->_bf = -1;
		sub1->_bf = 0;
	}
	else if (bf == -1)
	{
		//sub2左子树新增
		sub2->_bf = 0;
		parent->_bf = 0;
		sub1->_bf = 1;
	}
	else
	{
		assert(false);//即平衡因子出现错误的点,直接报错检查出来
	}
}

需要注意的点和第三种情况类似,在这里我不过多赘述了,直接看代码即可。
以上便是我们旋转的四种情况,根据上面的四种情况,我们对应相应的方法即可,对应的代码为:

//旋转
if (parent->_bf == 2 && cur->_bf == 1)//单纯的左单旋转
{
				RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)//单纯的右单旋转
{
				RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)//双向右旋转后左旋转
{
				RotateRL(parent);
}
else if(parent->_bf==-2&&cur->_bf==1)//双向左旋转后右旋转
{
				RotateLR(parent);
}
break;//调整一次后,对上面的根节点无影响了,恢复到插入前的高度了,不用再继续更新了,直接退出即可

AVL树的验证:

我们的AVL树构建好之后,接下来针对AVL树,我们需要进行一次关于AVL树的验证:
对于AVL树来说,其高度的平衡因子是我们最为关键的检验数据,因此我们的验证可以通过验证高度的方式来完成。
我们验证的方面主要有两个:
1.验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确

我们的代码如下:

void _InOrder(Node* root)//中序遍历
{
	if (root == nullptr)
	{
		return;
	}
	_InOrder(root->_left);
	cout << root->_kv.first << " ";
	_InOrder(root->_right);
}


int _Height(Node* root)//求树的高度,递归存储左右树数值比较
{
	if (root == nullptr)
	{
		return 0;
	}
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);

	return leftHeight > rightHeight ? 1 + leftHeight : 1 + rightHeight;
}


bool _IsBalance(Node* root)//判断是否为AVL树的函数,利用求二叉树的高度来判断
{
	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(leftheight - rightheight) < 2 && _IsBalance(root->_left) && _IsBalance(root->_right);
}

AVL树的性能分析:

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

总结:

以上便是关于AVL树的全部内容了,后续我会更新AVL树的删除节点的代码,希望在看完本文后,可以让你学会使用AVL树进行数据存储。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

golang实现正向代理和反向代理

文章目录 正向代理反向代理区别与联系:总结代理服务器实现正向代理反向代理正向代理 正向代理是客户端代理,它位于客户端和目标服务器之间。它的作用是保护客户端的隐私和安全。 如我们现在想要访问谷歌,但是由于某些原因,无法直接访问到谷歌,我们可以通过连接一台代理服务…

Redis缓存过期策略

文章目录 一、面试题二、redis内存1. Redis的内存大小怎么查看&#xff1f;2. 设置redis内存3. redis内存的OOM 三、redis内存淘汰策略1. redis的过期键删除策略2. redis缓存淘汰策略 一、面试题 1. 生产上你们redis内存设置多少&#xff1f; 2. 如何配置、修改redis内存大小…

YOLOV5 初体验:简单猫和老鼠数据集模型训练

1、前言 前两天&#xff0c;通过OpenCV 对猫和老鼠视频的抽取&#xff0c;提取了48张图片。这里不再介绍&#xff0c;可以参考之前的文章&#xff1a;利用OpenCV 抽取视频的图片&#xff0c;并制作目标检测数据集-CSDN博客 数据的目录如下&#xff1a; 项目的下载见文末 2、制…

基于Java的在线课程教学系统(Vue.js+SpringBoot)

目录 一、摘要1.1 系统介绍1.2 项目录屏 二、研究内容2.1 课程类型管理模块2.2 课程管理模块2.3 课时管理模块2.4 课程交互模块2.5 系统基础模块 三、系统设计3.1 用例设计3.2 数据库设计 四、系统展示4.1 管理后台4.2 用户网页 五、样例代码5.1 新增课程类型5.2 网站登录5.3 课…

第十一篇 - 应用于市场营销视频场景中的人工智能和机器学习技术 – Video --- 我为什么要翻译介绍美国人工智能科技巨头IAB公司(1)

IAB平台&#xff0c;使命和功能 IAB成立于1996年&#xff0c;总部位于纽约市。 作为美国的人工智能科技巨头社会媒体和营销专业平台公司&#xff0c;互动广告局&#xff08;IAB- the Interactive Advertising Bureau&#xff09;自1996年成立以来&#xff0c;先后为700多家媒体…

为什么选择 Flink 做实时处理

优质博文&#xff1a;IT-BLOG-CN 为什么选择 Flink 【1】流数据更真实地反映了我们的生活方式&#xff08;实时聊天&#xff09;&#xff1b; 【2】传统的数据架构是基于有限数据集的&#xff08;Spark 是基于微批次数据处理&#xff09;&#xff1b; 【3】我们的目标&#xf…

ROS——ROS环境搭建

Ubuntu 安装完毕后&#xff0c;就可以安装 ROS 操作系统了&#xff0c;大致步骤如下: 配置ubuntu的软件和更新&#xff1b; 设置安装源&#xff1b; 设置key&#xff1b; 安装&#xff1b; 配置环境变量。 1.配置ubuntu的软件和更新 配置ubuntu的软件和更新&#xff0c;…

系统编程--makefile项目管理

这里写目录标题 介绍语法结构总览基础规则简介最简单的makefile对于基础规则的理解和应用总结 makefile时尽量使用更独立的命令&#xff0c;减少文件之间的耦合度需求以及解决总结 补充&#xff08;关于makefile中脚本命令的编写顺序&#xff09; 一级目录二级目录二级目录二级…

数据科学中的Python:NumPy和Pandas入门指南【第121篇—NumPy和Pandas】

数据科学中的Python&#xff1a;NumPy和Pandas入门指南 数据科学是当今数字时代中的一个重要领域&#xff0c;而Python是数据科学家们最喜爱的编程语言之一。在这篇博客中&#xff0c;我们将介绍Python中两个强大的库——NumPy和Pandas&#xff0c;它们在数据处理和分析中发挥…

java算法第十八天 | ● 110.平衡二叉树 ● 257. 二叉树的所有路径 ● 404.左叶子之和

110.平衡二叉树 leetcode链接 思路&#xff1a; 使用后序遍历分别求左右子树的高度&#xff0c;若高度只差大于一&#xff0c;则返回-1&#xff0c;否则返回当前节点的最大高度。 /*** Definition for a binary tree node.* public class TreeNode {* int val;* Tree…

爬虫(五)

1. 前端JS相关 三元运算 v1 条件 ? 值A : 值B; # 如果条件成立v1值A&#xff0c;不成立v1等于值Bres 1 1 ? 99 : 88 # res99特殊的逻辑运算 v1 11 || 22 # Ture v2 9 || 14 # 9 v3 0 || 15 # 15 v3 0 || 15 || "zhangfei" # 15赋值和…

x86 Ubuntu上编译eudev给龙芯loongarch64架构主机使用

1、下载eudev库eudev-master.zip&#xff0c;链接&#xff1a;eudev库官方地址 2、下载龙芯的交叉编译工具&#xff1a;loongson-gnu-toolchain-8.3-x86_64-loongarch64-linux-gnu-rc1.2.tar.xz&#xff0c;链接&#xff1a;龙芯交叉编译官方地址 3、交叉编译器环境搭建 (1)、…

latex绘图中\begin{figure}[htbp]中的htbp什么意思

在LaTeX中&#xff0c;\begin{figure}[htbp] 用来开始一个图形环境&#xff0c;其中 [htbp] 是一个位置参数&#xff0c;用来指导LaTeX如何放置这个图形。 具体来说&#xff0c;[htbp] 中的每个字母代表一个放置选项&#xff1a; h&#xff1a;代表“here”&#xff0c;意味着…

【LeetCode: 299. 猜数字游戏 - 模拟 + 计数】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

springcloud第3季 consul服务发现注册,配置中心2

一 consul的作用 1.1 为何使用注册中心 为何要用注册中心&#xff1f; 1.A服务调用B服务&#xff0c;使用ip和端口&#xff0c;如果B服务的ip或者端口发生变化&#xff0c;服务A需要进行改动&#xff1b; 2.如果在分布式集群中&#xff0c;部署多个服务B&#xff0c;多个服…

Linux(Ubuntu)中安装vscode

①首先去vscode的官网下载.deb文件 网址&#xff1a;https://code.visualstudio.com/docs/?dvlinuxarm64_deb 注&#xff1a;如果linux端无法打开网页下载文件&#xff0c;可以在Windows端下载好用WinSCP传输到Linux。下载前注意下你的系统架构是arm还是amd&#xff0c;系统…

常用的加密算法

AES 高级加密标准&#xff08;AES, Advanced Encryption Standard&#xff09;是当今世界范围内应用最广泛的对称加密算法之一。在微信小程序加密传输等场景中&#xff0c;AES算法发挥着至关重要的作用。对称加密算法的特点在于加密和解密过程使用相同的密钥。具体来说&#x…

【MybatisPlus】BaseMapper详解,举例说明

一、BaseMapper 简介 MyBatis-Plus 的核心类 BaseMapper 主要是用于提供基本的 CRUD&#xff08;创建、读取、更新、删除&#xff09;操作的接口定义。它是 MyBatis-Plus 框架中的一个重要组成部分&#xff0c;可以大大简化基于 MyBatis 的数据访问层代码的编写。 BaseMapper…

设计模式—桥接模式

定义: 桥接模式是将抽象部分与它的实现部分分离&#xff0c;使它们都可以独立地变化。它是一种对象结构型模式&#xff0c;又称为柄体(Handle and Body)模式或接口(Interfce)模式。 本章代码:小麻雀icknn/设计模式练习 - Gitee.com 结构: 抽象化(Abstraction)角色&#xff1a…

MMCV报错

文章目录 mmcv1.x版本FAQ文档open-mmlab卸载环境中的mmcvImportError: cannot import name Config from mmcv (unknown location)problem description解决 ImportError: cannot import name mkdir_or_exist from mmcv.utils (unknown location)解决 AttributeError: module mmc…