C++【AVL树】

目录

1.AVL树(高度平衡搜索二叉树)定义

1.1平衡因子(Balance Factor简写成bf)

1.2avl树的定义

2.AVL树插入的基本操作

 2.1逻辑抽象图

2.2插入流程

2.3左单旋

2.4右单旋

2.5右左双旋

2.6左右双旋 

3.AVL树的检验技巧 

   3.1、检验依据

3.2、检验方法

3.3、AVL树的性能



1.AVL树(高度平衡搜索二叉树)定义

平衡二叉树 全称叫做 平衡二叉搜索(排序)树,简称 AVL树。英文:Balanced Binary Tree (BBT),注:二叉查找树(BST)

由 前苏联 的两位数学家:G.M.Adelson-Velskii 和 E.M.Landis 共同提出,首次出现在 1962 发布的论文 《An algorithm for the organization of information》 中。

AVL树在二叉搜索树的的基础上增加了两个特性:

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

1.1平衡因子(Balance Factor简写成bf)

定义:节点的右子树减去左子树的高度,在 AVL树中,所有节点的平衡因子都必须满足: -1<=bf<=1;

1.2avl树的定义

AVL 树在原 二叉搜索树 的基础上添加了 平衡因子 bf 以及用于快速向上调整的 父亲指针 parent,所以 AVL 树是一个三叉链结构:

 上图我们描述了一棵树的构建每个节点应该具备的属性,我们要想构建一颗avl树,我们就要以上图为基准,先创建节点,代码如下:

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

		AVLTreeNode* _left;
	    AVLTreeNode* _right; 
	    AVLTreeNode* _parent;
		std::pair<K, V>_kv;
		int bf; //平衡因子

};

然后就是组织的过程,我们只要创建根节点,然后增删改查就OK了,我们要注意平衡因子的约束,比如我们要按顺序插入1 2 3 4 5 6有了平衡因子的约束,插入题如下所示:

由上图可知,同样的结点,由于插入方式不同导致树的高度也有所不同。特别是在带插入结点个数很多且正序的情况下,会导致二叉树的高度是O(N),而AVL树就不会出现这种情况,树的高度始终是O(lgN).高度越小,对树的一些基本操作的时间复杂度就会越小。这也就是我们引入AVL树的原因。

2.AVL树插入的基本操作

 插入节点的基本操作跟搜索二叉树差不多,但是需要收到平衡因子的约束,下面是插入过程平衡因子的注意事项;

  1. 新增在左,parent平衡因子减减;
  2. 新增在右,parent平衡因子加加;
  3. 更新后parent平衡因子 == 0,说明parent所在的子树高度不变,不会影响祖先,不用再继续沿着root的路径网上更新;
  4. 更新后parent平衡因子 == 1 or -1,说明parent所在的子树的高度变化,会影响祖先,需要继续沿着到root往上更新;
  5. 更新后parent平衡因子 == 2 or -2,说明parent所在的子树高度变化且不平衡,对parent所在的子树进行旋转,让他平衡;
  6. 更新到root节点;

 2.1逻辑抽象图

AVL 树的 旋转操作 比较复杂,需要考虑多种形状、多种情况,为了方便理解,将 部分节点 视为一个整体(抽象化),主要看高度 h进行旋转操作,可以得出下面这个抽象图

通过逻辑图可以方便我们对avl树插入的情况做清晰的分析。

2.2插入流程

  1. 判断根是否为空,如果为空,则进行第一次插入,成功后返回 true
  2. 找到合适的位置进行插入,如果待插入的值比当前节点值大,则往 右 路走,如果比当前节点值小,则往 左 路走
  3. 判断父节点与新节点的大小关系,根据情况判断链接至 左边 还是 右边
  4. 更新平衡因子,然后判断是否需要进行 旋转 调整高度

代码框架如下,具体旋转情况具体分析实现 

//插入节点
bool Insert(const std::pair<K, V> kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}

	//易错点:没有提前记录父亲
	Node* parent = nullptr;
	Node* cur = _root;
	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;
	}
	
	//创建新节点,链接
	cur = new Node(kv);
	if (parent->_kv.first < kv.first)
	{
		parent->_right = cur;
		cur->_parent = parent;
	}
	else
	{
		parent->_left = cur;
		cur->_parent = parent;
	}

	//根据平衡因子判断是否需要旋转
	while (parent)
	{
		//更新平衡因子
		if (parent->_right == cur)
			parent->_bf++;
		else
			parent->_bf--;

		//判断是否需要调整
		//……
	}
	
	return true;
}

 

2.3左单旋

左单旋的适用场景如下:在根的右子树中出现 平衡因子 为 1 的情况下,仍然往右侧插入节点,插入后会导致 右子树 中某个节点 平衡因子 值为 2 ,此时就需要使用 左单旋 降低高度。

在Z的右枝插入节点会导致z的平衡因子++,R的平衡因子会增加成2,则不满足avl树的要求,我们就要对R节点左旋,具体代码如下:

//左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	//先将 subR 的左孩子移交给父亲
	parent->_right = subRL;
	if (subRL != nullptr)
		subRL->_parent = parent;

	Node* pparent = parent->_parent;

	//易错点:忘记更改原父亲的链接关系
	subR->_left = parent;
	parent->_parent = subR;

	//再将父亲移交给 subR,subR 成为新父亲
	if (parent == _root)
	{
		//如果原父亲为根,那么此时需要更新 根
		subR->_parent = nullptr;
		_root = subR;
	}
	else
	{
		//单纯改变链接关系
		if (pparent->_right == parent)
			pparent->_right = subR;
		else
			pparent->_left = subR;

		subR->_parent = pparent;
	}

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

旋转其实就是改变链接过程,更改的链接示意图如下

助记:搜索二叉树的右数>左树 则     1.parent->right = cur->left 2.cur->left = parent

注意: subRL 可能是 nullptr,在改变其链接关系时,需要判断一下,避免空指针解引用行为;parent 可能是 根节点,subR 在链接后,需要更新 根节点;左单旋后,parentsubR 的平衡因子都可以更新为 0,此时是很平衡的

2.4右单旋

右单旋的适用场景如下:在根的左子树中出现 平衡因子 为 1 的情况下,仍然往左侧插入节点,插入后会导致 左子树 中某个节点 平衡因子 值为 2 ,此时就需要使用 右单旋 降低高度

右单旋 的场景与 左单旋 如出一辙,不过方向不同而已

右单选的代码,复制左单旋改变链接指向就行

//右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	//先将 subL 的右孩子移交给父亲
	parent->_left = subLR;
	if (subLR != nullptr)
		subLR->_parent = parent;

	Node* pparent = parent->_parent;

	subL->_right = parent;
	parent->_parent = subL;

	//再将父亲移交给 subL,subL 成为新父亲
	if (parent == _root)
	{
		//如果原父亲为根,那么此时需要更新 根
		subL->_parent = nullptr;
		_root = subL;
	}
	else
	{
		//单纯改变链接关系
		if (pparent->_right == parent)
			pparent->_right = subL;
		else
			pparent->_left = subL;

		subL->_parent = pparent;
	}

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

助记:搜索二叉树的右数>左树 则     1.parent->left = cur->right 2.cur-> right = parent

注意: subLR 可能是 nullptr,在改变其链接关系时,需要判断一下,避免空指针解引用行为;parent 可能是 根节点,subL 在链接后,需要更新 根节点;右单旋后,parentsubLR 的平衡因子都可以更新为 0,此时是很平衡的

2.5右左双旋

当值插入 右子树的右侧 时,可能引发 左单旋,当值插入 左子树的左侧 时,则可能引发 右单旋

如果插入的是 右子树的左侧 或 左子树的右侧 时,则可能引发 双旋

比如 插入右子树的左侧 时,单单凭借 左单旋 无法解决问题,需要 先进行 右单旋,再进行 左单旋 才能 降低高度,这一过程就成为 双旋(右左双旋)

//右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int BF = subRL->_bf;

	//先右单旋
	RotateR(subR);

	//再左单旋
	RotateL(parent);

	//根据不同的情况更新平衡因子
	if(BF == 0)
	{
		parent->_bf = subR->_bf = 0;
	}
	else if (BF == 1)
	{
		parent->_bf = -1;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (BF == -1)
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else
	{
		//非法情况
		std::cerr << "此处的平衡因子出现异常!" << std::endl;
		assert(false);	//直接断言报错
	}
}

右左双旋 的抽象图 旋转 流程如下(动图)

注:双旋 部分的动图省略了部分细节,着重展现 高度降低 的现象

 

右左双旋 逻辑:

确定 parent、subR、subRL
subRL 的右子树托付给 subR,左子树托付给 parent
subRL 向上提,整体高度下降
需要特别注意平衡因子的调整
双旋 的 平衡因子 调整需要分类讨论:
情况一:新节点插入至右子树左侧后,subRL 平衡因子变为 0,此时树变得更加平衡了,因此 parent、subR、subRL 三者的平衡因子都为 0

情况二:新节点插入至右子树的左侧后,subRL 平衡因子变为 -1,证明 新节点插入至 subRL 的左边,并且右边没有东西,旋转后,将新节点托付给 parent 后,parent 变得平衡了,但 subR 因没有分到节点,因此导致其左侧失衡,平衡因子变为 1subRL 平衡,为 0(这其实就是动图展示的情况)

情况三:新节点插入至右子树的左侧后,subRL 平衡因子变为 1,证明 新节点插入至 subRL 的右边,并且左边没有东西,旋转后,parent 没有分到节点,subR 分到了,subRL 为平衡,因此 parent 的平衡因子为 -1,subR subRL 的平衡因子都是 0

经过这样分析后,就能得到代码中的判断逻辑

注意: 先要右单旋,才左单旋;平衡因子的更新需要分类讨论

 

2.6左右双旋 

当节点插入至 左子树的右侧 时,会触发 左右双旋,需要 先进行 左单旋,再进行 右单旋 才能降低高度

//左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int BF = subLR->_bf;

	//先左单旋
	RotateL(subL);

	//再右单旋
	RotateR(parent);

	//根据不同的情况更新平衡因子
	if (BF == 0)
	{
		parent->_bf = subL->_bf = 0;
	}
	else if (BF == 1)
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	else if (BF == -1)
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
	{
		//非法情况
		std::cerr << "此处的平衡因子出现异常!" << std::endl;
		assert(false);	//直接断言报错
	}
}

左右双旋 的 旋转 流程如下图所示(动图)

 

 

左右双旋 逻辑:

确定 parent、subL、subLR
将 subLR 的右子树托付给 parent,左子树托付给 subL
subLR 向上提,整体高度下降
需要特别注意平衡因子的调整
调整逻辑与 右左双旋 差不多

情况一:新节点插入至左子树右侧后,subLR 平衡因子变为 0,此时树变得更加平衡了,因此 parent、subL、subLR 三者的平衡因子都为 0

情况二:新节点插入至左子树的右侧后,subLR 平衡因子变为 -1,证明 新节点插入至 subLR 的左边,并且右边没有东西,旋转后,将新节点托付给 subL 后,subL 变得平衡了,但 parent 因没有分到节点,因此导致其右侧失衡,平衡因子变为 1,subLR 平衡,为 0

情况三:新节点插入至左子树的右侧后,subLR 平衡因子变为 1,证明 新节点插入至 subLR 右边,并且左边没有东西,旋转后,subL 没有分到节点,parent 分到了,subLR 为平衡,因此 subL 的平衡因子为 -1,parent 和 subLR 的平衡因子都是 0(动图中演示的就是情况三)

总的来说,双旋 需要慎重考虑 平衡因子 的调整

 小结:旋转情况:

  • 插入至 右右 时,左单旋
  • 插入至 左左 时,右单旋
  • 插入至 右左 时,右左双旋
  • 插入至 左右 时,左右双旋

3.AVL树的检验技巧 

   3.1、检验依据

 如何检验自己的 AVL 树是否合法? 答案是通过平衡因子检查 

平衡因子 反映的是 左右子树高度之差,计算出 左右子树高度之差 与当前节点的 平衡因子 进行比对,如果发现不同,则说明 AVL 树 非法或者如果当前节点的 平衡因子 取值范围不在 [-1, 1] 内

3.2、检验方法

统计 二叉树子树高度 很简单,只需要在 检验合法性函数 中调用即可

 

//验证是否为 AVL 树
bool IsAVLTree()
{
	return _IsAVLTree(_root);
}

//获取高度
size_t getHeight()
{
	return _getHeight(_root);
}

bool _IsAVLTree(Node* root)
{
	if (root == nullptr)
		return true;

	//计算左右子树的高度
	size_t leftTreeH = _getHeight(root->_left);
	size_t rightTreeH = _getHeight(root->_right);

	//计算差值
	int diff = rightTreeH - leftTreeH;
	if (diff != root->_bf || root->_bf < -1 || root->_bf > 1)
	{
		std::cerr << "当前节点出现了问题: " << root->_kv.first<< " | " << root->_bf << std::endl;
		return false;
	}

	return _IsAVLTree(root->_left) && _IsAVLTree(root->_right);
}

size_t _getHeight(Node* root)
{
	if (root == nullptr)
		return 0;

	size_t leftH = _getHeight(root->_left);
	size_t rightH = _getHeight(root->_right);

	return 1 + std::max(leftH, rightH);
}

通过一段简单的代码,随机插入 10000 个节点,判断 是否合法 及当 AVL 树的 高度

void AVLTreeTest2()
{
	srand((size_t)time(NULL));

	AVLTree<int, int> av;

	for (int i = 0; i < 10000; i++)
	{
		int val = rand() % 10000 + i;
		av.Insert(val, val);
	}

	cout << "检查AVL树: " << av.IsAVLTree() << endl << "高度为:" << av.getHeight() << endl;
}

3.3、AVL树的性能

AVL 树是一棵 绝对平衡 的二叉树,对高度的控制极为苛刻,稍微有点退化的趋势,都要被旋转调整,这样做的好处是 严格控制了查询的时间,查询速度极快,约为 logN

但是过度苛刻也会带来一定的负面影响,比如涉及一些 结构修改 的操作时,性能非常低下,更差的是在 删除 时,因为从任意位置破坏了 二叉搜索树 及 AVL 树的属性,有可能会引发连锁旋转反应,导致一直 旋转 至 根 的位置(旋转比较浪费时间)

AVL 树性能很优秀,如果在存储大量不需要修改的静态数据时,用 AVL 树是极好的,但在大多数场景中,用不到这么极限的性能,此时就需要一种 和 AVL 树差不多,但又没有那么严格 的 平衡二叉搜索树 了

 

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

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

相关文章

AI:127-基于卷积神经网络的交通拥堵预测

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的关键代码,详细讲解供…

2024-02-13 Unity 编辑器开发之编辑器拓展4 —— EditorGUIUtility

文章目录 1 EditorGUIUtility 介绍2 加载资源2.1 Eidtor Default Resources2.2 不存在返回 null2.3 不存在则报错2.4 代码示例 3 搜索框查询、对象选中提示3.1 ShowObjectPicker3.2 PingObject3.3 代码示例 4 窗口事件传递、坐标转换4.1 CommandEvent4.2 GUIPoint 和 ScreenPoi…

Excel模板2:进度条甘特图

Excel模板2&#xff1a;进度条甘特图 ‍ 今天复刻B站up【名字叫麦兜的狗狗】的甘特图&#xff1a;还在买Excel模板吗&#xff1f;自己做漂亮简洁的甘特图吧&#xff01;_哔哩哔哩_bilibili 阿里网盘永久分享&#xff1a;https://www.alipan.com/s/cXhq1PNJfdm 当前效果&…

片上网络NoC(5)——非直连拓扑

目录 一、前言 二、概念阐述 三、交叉开关 四、蝶形网络 五、clos网络 六、fat tree网络 6.1 clos网络的折叠过程 七、总结 一、前言 本文继续介绍片上网络的拓扑&#xff0c;在之前的文章中&#xff0c;我们已经介绍了片上网络的拓扑指标和直连拓扑的相关内容&#xf…

【新年第一辑 | TortoiseGit常见用法】

TortoiseGit常见用法 概述常用操作建立仓库提交代码更新代码回滚版本添加忽略文件设置比较工具&#x1fa78; 解决冲突 主页传送门 &#xff1a; &#x1f4c0; 传送 概述 TortoiseGit是一个Windows平台上的Git客户端工具&#xff0c;它提供了一个直观和易于使用的图形用户界面…

面向对象2:继承

目录 2.1继承 2.2 继承的好处 2.3 权限修饰符 2.4 单继承、Object 2.5 方法重写 2.6 子类中访问成员的特点 2.7 子类中访问构造器的特点 面向对象1&#xff1a;静态 2.1继承 向对象编程之所以能够能够被广大开发者认可&#xff0c;有一个非常重要的原因&#xff0c;是…

springboot184基于springboot的校园网上店铺的设计与实现

简介 【毕设源码推荐 javaweb 项目】基于springbootvue 的 适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负。 看运行截图看 第五章 第四章 获取资料方式 **项…

如何把华为手机上的数据转移到荣耀手机上?

方法/步骤 点击并进入华为手机&#xff08;旧手机&#xff09;的【手机克隆】应用&#xff0c;选择【这是旧设备】&#xff1b; 点击并进入荣耀手机&#xff08;新手机&#xff09;的【换机克隆】应用&#xff0c;选择【这是新设备】&#xff1b; 荣耀手机&#xff08;新…

LeetCode-第70题-爬楼梯

1.题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 2.样例描述 3.思路描述 画图就可以发现规律&#xff0c;典型的斐波那契额数列 4.代码展示 class Solution {public int climbStair…

webgis后端安卓系统部署攻略,超详细Termux攻略

目录 前言 一、将后端项目编译ARM64 二、安卓手机安装termux 1.更换为国内源 2.安装ssh远程访问 3.安装文件远程访问 三、安装postgis数据库 1.安装数据库 2.数据库配置 3.数据导入 四、后端项目部署 五、自启动设置 总结 前言 因为之前一直做的H5APP开发&#xf…

机器学习、深度学习、强化学习、迁移学习的关联与区别

Hi&#xff0c;大家好&#xff0c;我是半亩花海。本文主要了解并初步探究机器学习、深度学习、强化学习、迁移学习的关系与区别&#xff0c;通过清晰直观的关系图展现出四种“学习”之间的关系。虽然这四种“学习”方法在理论和应用上存在着一定的区别&#xff0c;但它们之间也…

2024幻兽帕鲁服务器创建教程_阿里PK腾讯超简单

幻兽帕鲁官方服务器不稳定&#xff1f;自己搭建幻兽帕鲁服务器&#xff0c;低延迟、稳定不卡&#xff0c;目前阿里云和腾讯云均推出幻兽帕鲁专用服务器&#xff0c;腾讯云直接提供幻兽帕鲁镜像系统&#xff0c;阿里云通过计算巢服务&#xff0c;均可以一键部署&#xff0c;鼠标…

深度学习-吴恩达L1W2作业

作业1&#xff1a;吴恩达《深度学习》L1W2作业1 - Heywhale.com 作业2&#xff1a;吴恩达《深度学习》L1W2作业2 - Heywhale.com 作业1 你需要记住的内容&#xff1a; -np.exp&#xff08;x&#xff09;适用于任何np.array x并将指数函数应用于每个坐标 -sigmoid函数及其梯度…

【编程题】合法括号的判断

合法括号的判断—难度&#xff1a;⭐⭐ 我的答案&#xff1a;错误 class Parenthesis {public:bool chkParenthesis(string A, int n) { // write code hereif (n % 2 ! 0) {return false;}stack<char> st;auto ch A.begin(); // cout<<"hello?"<&l…

react渲染流程是怎样的

整体流程&#xff1a; react的核心可以用uifn(state)来表示&#xff0c;更详细可以用&#xff1a; const state reconcile(update); const UI commit(state);上面的fn可以分为如下一个部分&#xff1a; Scheduler&#xff08;调度器&#xff09;&#xff1a; 调度任务&…

Netty应用(十一) 之 ChannelHandler Channel生命周期 @Sharable 心跳

目录 27.ChannelHandler总结 27.1 一些概念 27.2 到底有几个handler&#xff1f;真的只有你想的那样吗&#xff1f; 27.3 channel.writeAndFlush 和 ctx.writeAndFlush的区别 27.4 ByteBuf的创建和销毁 27.5 Channel的生命周期方法 27.5.1 handlerAdded 27.5.2 channelR…

VS Code主题设置(美化VS Code)

主题的具体效果放在了文章末尾&#xff0c;这篇文章后续也会进行更新 目录 切换整体主题&#xff08;整体主题&#xff09; 1.VS Code内置主题&#xff08;快捷键&#xff1a;CtrlK &#xff0c;CtrlT&#xff09; 1.VS Code左上角点击文件 2.选择首选项-->主题-->颜色…

理解JAVA EE设计模式

理解JAVA EE设计模式 在Web应用程序的设计和开发阶段,开发人员在开发类似的项目时可能会遇到相似的问题。每名开发人员可能会遇到的问题找出不同或相似的解决方案。但是,这导致一些时间和精力浪费在为相似的问题寻找解决方案上。因此,要啊节省时间和精力,需要记录常见问题…

【Pyhton4Delpi】学习笔记(二)安装验证篇

D12环境下安装P4D。 一、下载 Python4Delphi&#xff08;下称P4D&#xff09;: 下载地址&#xff1a;https://github.com/pyscripter/python4delphi 下载或者克隆P4D到指定的目录&#xff0c;例如&#xff1a;MDS_New&#xff0c;目录结构如下&#xff0c;P4D就是克隆下来的…

localStorage、sessionStorage、cookie区别

localStorage: localStorage 的生命周期是永久的&#xff0c;关闭页面或浏览器之后 localStorage 中的数据也不会消失。localStorage 除非主动删除数据&#xff0c;否则数据永远不会消失 sessionStorage: sessionStorage 的生命周期是仅在当前会话下有效。sessionStorage 引入…