C++-第十二章: AVL树

目录

第一节:AVL树的特征

第二节:实现思路

        2-1.插入

                2-1-1.右单旋

                2-1-2.左单旋

                2-1-3.左右双旋

                2-1-4.右左双旋

                2-1-5.总结

        2-2.删除

第三节:代码实现

        3-1.Node类

        3-2.AVLTree类

                3-2-1.Insert函数

                3-2-2.Height函数

                3-2-3.Balance函数

                3-2-4.其他函数

                 3-2-5.clear函数

第四节:测试

第五节:优化方案

gitee:AVL树 · 转调/C++ - 码云 - 开源中国 

第六章:总结

下期预告:


第一节:AVL树的特征

        如果一个搜索二叉树的结构如下:

                                

        那么当我想找a的时候,时间复杂度就从 log₂n 退化成 n 了。

        究其原因,搜索二叉树没有一个机制让树的整体高度变得更低。

        所以就出现了AVL树,AVL树通过保证每个节点的左右子树高度差不超过[-1,1],保证整棵树的平衡。

        将左右子树高度差抽象成平衡因子,平衡因子=右子树高度-左子树高度,每个节点都要维护自己的平衡因子不超过1。

第二节:实现思路

        2-1.插入

        1.首先按照搜索二叉树的规则进行插入

        2.插入后更新其父亲节点的平衡因子

                a.插入父亲节点的左子树,父亲节点平衡因子--

                b.插入父亲节点的右子树,父亲节点平衡因子++

        3.如果父亲节点平衡因子是-1或者1,说明父亲所在子树的高度改变了,继续更新父亲的父亲节点的平衡因子,若父亲的父亲节点的平衡因子也为-1或者1,继续向上更新,直到平衡因子不为-1或者1;

        如果某个祖先节点的平衡因子更新后为0,说明祖先节点所在子树高度不变了,对祖先之上的节点也没有影响了,不再往上更新;

        如果某个祖先节点的平衡因子为-2或者2,就需要进行旋转处理了:

                2-1-1.右单旋

        当前节点平衡因子为-2 && 其左孩子的平衡因子为-1 进行右单旋

        用长方形表示子树高度,b、c的高度都为h,a的高度为h+1,红色方块为节点新增位置,蓝色字体为节点的平衡因子:

                                        

        60的平衡因子为-2,并且其左孩子30的平衡因子为-1,进行右单旋

        具体的方法是将60的左孩子变成30的右孩子,30的右孩子被"抢走了",就把60变成30的右孩子,最后把60的父亲变成30的父亲。

        这样做之后30和60的平衡因子都变成了0,就不需要再向上更新了。

        可以发现30向右旋转把60顶替了,60向右旋转屈居30,a、b、c从左向右的顺序依然不变。

                2-1-2.左单旋

        当前节点平衡因子为+2 && 其右孩子的平衡因子为+1

                                                

        30的平衡因子为+2,并且其右孩子60的平衡因子为+1,进行左单旋

        

        这样做之后30和60的平衡因子都变成了0,就不需要再向上更新了。

        可以发现60向左旋转把30顶替了,30向左旋转屈居30,a、b、c从左向右的顺序依然不变。

                2-1-3.左右双旋

        当前节点平衡因子为-2 && 其左孩子的平衡因子为+1时进行左右双旋。

        左右双旋有三种不同的情况,它们的旋转逻辑是一样的,但是最后的平衡因子不同。

        旋转逻辑:

        先不看它们的平衡因子,只看节点和子树的交换关系,此时a=h,d=h,b和c有一个等于h-1,另一个等于h,才能保证90的平衡因子为-2,30的平衡因子为1。

            其实就是孙子变成了它爸爸和爷爷共同的父亲,然后仍然保持a、b、c、d的左右顺序 

        (1)如果60自己就是新增的节点

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​    

        

        3个节点的平衡因子都为0,不再向上更新。

        为了便于理解,我使用绿色字母表示新增节点后a、b、c、d的高度。

        (2)如果60的左子树新增节点

        此时60的平衡因子为0,不再向上更新,30的平衡因子为0,90的平衡因子为1

        (3)如果60的右子树新增节点

        此时60的平衡因子为0,不再向上更新,30的平衡因子为-1,90的平衡因子为0

                2-1-4.右左双旋

        当前节点平衡因子为+2 && 其右孩子的平衡因子为-1时进行左右双旋。

        左右双旋也有三种不同的情况,它们的旋转逻辑是一样的,但是最后的平衡因子不同。

         旋转逻辑:

        

        也是孙子变成了它爸爸和爷爷共同的父亲,然后仍然保持a、b、c、d的左右顺序 ,这和左右双旋的结果一致。 

         所以如果是60的左子树b新增节点,旋转后,30、60、90的平衡因子分别是0、0、1;

        如果是60的右子树c新增节点,旋转后,30、60、90的平衡因子分别是-1、0、0;

        如果60本身是新增的节点,旋转后,30、60、90的平衡因子都是0。

                2-1-5.总结

        只要进行旋转后,"辈分"最高的节点的平衡因子都变成了0,说明只要进行过旋转,就不需要再往上更新了。

第三节:代码实现

        将以上思路整理成代码。

        3-1.Node类

        首先创建一个节点类:

        它除了节点的连接和保存的值之外,还需要一个变量_bf来保存平衡因子。

template<class T>
class Node
{
public:
	Node<T>* _left = nullptr;
	Node<T>* _right = nullptr;
	Node<T>* _parent = nullptr;
	T _val;
	short _bf = 0;
};

        3-2.AVLTree类

        它是AVL树的具体实现。

class AVLTree
{
private:
	Node<T>* _root = nullptr; // 保存根节点
};

                3-2-1.Insert函数

        首先完成插入函数,它传入一个值,然后按照搜索二叉树的思路插入节点:

		void Insert(const T& val)
		{
			// 如果根节点为空,赋值根节点即可
			if (_root == nullptr)
			{
				_root = new Node<T>;
				_root->_val = val;
				return;
			}
			Node<T>* cur = _root;
			Node<T>* parent = nullptr;
			while (cur)
			{
				if (cur->_val < val)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_val > val)
				{
					parent = cur;
					cur = cur->_left;
				}
			}

			cur = new Node<T>;
			cur->_val = val;
			if (parent->_val < val)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			cur->_parent = parent;
			
			// 更新祖先节点的平衡因子
			while (parent)
			{
				if (cur == parent->_left)
				{
					parent->_bf--;
				}
				else
				{
					parent->_bf++;
				}
				// 继续更新
				if (abs(parent->_bf) == 1)
				{
					cur = parent;
					parent = parent->_parent;
					continue;
				}
				// 不再更新
				if (parent->_bf == 0)
				{
					break;
				}
				// 如果失衡,进行旋转
				if (abs(parent->_bf) > 1)
				{
					Rotate(parent);
					break;
				}
			}
		}

                3-2-2.Height函数

        它获取某个节点的高度,获得某个节点左右孩子的高度后,就可以计算_bf:

		size_t _Height(Node<T>* root)
		{
			if (root == nullptr) return 0;
			return std::max(_Height(root->_left), _Height(root->_right)) + 1;
		}
		size_t Height(Node<T>* root)
		{
			return _Height(root);
		}

                3-2-3.Balance函数

        AVL树的核心函数,它用来更新节点的平衡因子:

void Rotate(Node<T>* cur)
{
		// 进行旋转处理,旋转完也不需要继续更新了
		if (cur->_bf == -2 && cur->_left->_bf == -1) // 右单旋
		{
			Node<T>* P = cur;      // 父亲
			Node<T>* S = P->_left; // 儿子
			P->_left = S->_right;
			if (S->_right)
				S->_right->_parent = P;
			S->_right = P;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = S;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = S;
				}
			}
			else
			{
				_root = S;
			}
			S->_parent = P->_parent;
			P->_parent = S;
			P->_bf = 0;
			S->_bf = 0;
		}
		else if (cur->_bf == 2 && cur->_right->_bf == 1) // 左单旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_right;
			P->_right = S->_left;
			if (S->_left)
				S->_left->_parent = P;
			S->_left = P;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = S;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = S;
				}
			}
			else
			{
				_root = S;
			}
			S->_parent = P->_parent;
			P->_parent = S;
			P->_bf = 0;
			S->_bf = 0;
		}
		else if (cur->_bf == -2 && cur->_left->_bf == 1) // 左右双旋
		{
			Node<T>* P = cur;      // 父亲
			Node<T>* S = P->_left; // 儿子
			Node<T>* G = S->_right;// 孙子
			P->_left = G->_right;
			if (G->_right)
				G->_right->_parent = P;
			G->_right = P;

			S->_right = G->_left;
			if (G->_left)
				G->_left->_parent = S;
			G->_left = S;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = G;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = G;
				}
			}
			else
			{
				_root = G;
			}
			G->_parent = P->_parent;
			P->_parent = G;
			S->_parent = G;
			if (G->_bf == 1)
			{
				S->_bf = -1;
				P->_bf = 0;
			}
			else if (G->_bf == -1)
			{
				S->_bf = 0;
				P->_bf = 1;
			}
			else if (G->_bf == 0)
			{
				S->_bf = P->_bf = 0;
			}
			G->_bf = 0;
		}
		else if (cur->_bf == 2 && cur->_right->_bf == -1) // 右左双旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_right;
			Node<T>* G = S->_left;
			P->_right = G->_left;
			if (G->_left)
				G->_left->_parent = P;
			G->_left = P;

			S->_left = G->_right;
			if (G->_right)
				G->_right->_parent = S;
			G->_right = S;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = G;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = G;
				}
			}
			else
			{
				_root = G;
			}
			G->_parent = P->_parent;
			P->_parent = G;
			S->_parent = G;
			if (G->_bf == 1)
			{
				S->_bf = 0;
				P->_bf = -1;
			}
			else if (G->_bf == -1)
			{
				S->_bf = 1;
				P->_bf = 0;
			}
			else if (G->_bf == 0)
			{
				S->_bf = P->_bf = 0;
			}
			G->_bf = 0;
		}
}

                3-2-4.其他函数

        Print 和 IsBalance 用来按升序打印内容和判断每个节点是否都平衡:

	bool IsBalance()
	{
		return _IsBalance(_root);
	}
	void Print()
	{
		_Print(_root);
	}

	bool _IsBalance(Node<T>* root)
	{
		if (root == nullptr) return true;
		if (abs(root->_bf) > 1) return false;
		return _IsBalance(root->_left) && _IsBalance(root->_right);
	}
	void _Print(Node<T>* root)
	{
		if (root == nullptr) return;
		_Print(root->_left);
		std::cout << root->_val << " ";
		_Print(root->_right);
	}

                 3-2-5.clear函数

        因为节点都是new出来的,所以使用清理函数释放空间,析构函数也要调用这个函数:

		void _clear(Node<T>* root)
		{
			if (root == nullptr) return;
			_clear(root->_left);
			_clear(root->_right);
			delete root;
		}
        void clear()
		{
			_clear(_root);
		}
		~AVLTree()
		{
			clear();
		}
		

第四节:测试

        插入多个随机数,然后调用 Print 和 IsBalance。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#include "AVLTree.hpp"

int main()
{
	zd::AVLTree<int> tree;
	srand(time(NULL));
	int i = 100;
	while (i--)
	{
		int x = rand();
		tree.Insert(x);
	}
	tree.Print();
	if (tree.IsBalance())
	{
		printf("true\n");
	}
	else
	{
		printf("false\n");
	}
	return 0;
}

        观察打印结果和最后是否为true即可。

第五节:优化方案

        实际上左右双旋和右左双旋就是左单旋和右单旋的组合,所以可以将左单旋、右单旋封装成函数,供左右单旋和右左单旋调用。

        但是封装的时候G的_bf需要提前保存,因为调用两次单旋之后,G的_bf就改变了:

		void RotateR(Node<T>* cur) // 右单旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_left;
			P->_left = S->_right;
			if (S->_right)
				S->_right->_parent = P;
			S->_right = P;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = S;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = S;
				}
			}
			else
			{
				_root = S;
			}
			S->_parent = P->_parent;
			P->_parent = S;
			P->_bf = 0;
			S->_bf = 0;
		}

		void RotateL(Node<T>* cur) // 左单旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_right;
			P->_right = S->_left;
			if (S->_left)
				S->_left->_parent = P;
			S->_left = P;
			if (P != _root)
			{
				if (P == P->_parent->_left)
				{
					P->_parent->_left = S;
				}
				else if (P == P->_parent->_right)
				{
					P->_parent->_right = S;
				}
			}
			else
			{
				_root = S;
			}
			S->_parent = P->_parent;
			P->_parent = S;
			P->_bf = 0;
			S->_bf = 0;
		}

		void RotateLR(Node<T>* cur) // 左右双旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_left;
			Node<T>* G = S->_right;
			short bf = G->_bf;

			RotateL(S);
			RotateR(P);

			if (bf == 1)
			{
				S->_bf = -1;
				P->_bf = 0;
			}
			else if (bf == -1)
			{
				S->_bf = 0;
				P->_bf = 1;
			}
			else if (bf == 0)
			{
				S->_bf = P->_bf = 0;
			}
			G->_bf = 0;
		}

		void RotateRL(Node<T>* cur) // 右左双旋
		{
			Node<T>* P = cur;
			Node<T>* S = P->_right;
			Node<T>* G = S->_left;
			short bf = G->_bf;

			RotateR(S);
			RotateL(P);

			if (bf == 1)
			{
				S->_bf = 0;
				P->_bf = -1;
			}
			else if (bf == -1)
			{
				S->_bf = 1;
				P->_bf = 0;
			}
			else if (bf == 0)
			{
				S->_bf = P->_bf = 0;
			}
			G->_bf = 0;
		}

        用以上接口替换Rotate中的对应代码即可。

Gitee:AVL树 · 转调/C++ - 码云 - 开源中国 

第六章:总结

        单旋时,向左会被旋转到向右,向右会被旋转到向左,但是a、b、c的顺序不变

        双旋时,两种双旋的结果一样,a、b、c、d的顺序也不变,而且双旋可以拆分成两次单旋:S先旋、P后旋。 

        负二左负右单旋,反之正一左右旋;正二右正左单旋,反之负一右左旋;两个双旋子先动;a、b、c、d顺序齐。 

下期预告:

        第十三章将介绍另外一种搜索二叉树——红黑树。

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

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

相关文章

学习路程八 langchin核心组件 Models补充 I/O和 Redis Cache

前序 之前了解了Models&#xff0c;Prompt&#xff0c;但有些资料又把这块与输出合称为模型输入输出&#xff08;Model I/O&#xff09;‌&#xff1a;这是与各种大语言模型进行交互的基本组件。它允许开发者管理提示&#xff08;prompt&#xff09;&#xff0c;通过通用接口调…

【fnOS飞牛云NAS本地部署DeepSeek-R1结合内网穿透远程访问告别服务器繁忙】

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

ISIS(中间系统到中间系统)——基础

ISIS是一项通用的动态路由协议&#xff0c;其隶属于链路状态路由协议&#xff0c;最初运行与OSI七层的网络层&#xff0c;采用组播地址224.0.0.14和224.0.0.15两个组波段&#xff0c;由于其较高的拓展性与高速收敛&#xff0c;被大多数运营商网络所使用 起源 ISIS最初是由国际…

DeepSeek本地部署:开启智能搜索的本地之旅

前言 嘿&#xff0c;朋友们&#xff01;最近国产大模型DeepSeek特别火&#xff0c;以至于频繁出现反应迟缓甚至宕机的情况&#xff0c;和两年前ChatGPT刚推出时的遭遇颇为相似。这让我想起了那句老话&#xff1a;“自己动手&#xff0c;丰衣足食”。万幸的是&#xff0c;DeepSe…

初会学习记录

【25初级会计《实务》】第一章&#xff1a;权责发生制举例_哔哩哔哩_bilibili 务实&#xff1a; 第一章 (1)会计概念&#xff0c;职能和目标&#xff1a; 2025年2月25日&#xff1a; (2)会计假设&#xff1a; 2025年2月26日&#xff1a; (3)会计核算基础&#xff1a; 202…

STM32——HAL库开发笔记22(定时器3—呼吸灯实验)(参考来源:b站铁头山羊)

本文利用前几节所学知识来实现一个呼吸灯实验&#xff1a;两颗led灯交替呼吸。 一、STM32CubeMX配置 step1&#xff1a;配置调试接口 step2&#xff1a;配置定时器 定时器1位于APB2总线上&#xff0c;如上图所示。 step3&#xff1a;配置时基单元 按照下图配置 时钟来源配置…

深度剖析数据中台架构图,铸造数字文明的基石

🔥🔥 AllData大数据产品是可定义数据中台,以数据平台为底座,以数据中台为桥梁,以机器学习平台为中层框架,以大模型应用为上游产品,提供全链路数字化解决方案。 ✨奥零数据科技官网:http://www.aolingdata.com ✨AllData开源项目:https://github.com/alldatacenter/a…

物联网通信应用案例之《智慧农业》

案例概述 在智慧农业方面&#xff0c;一般的应用场景为可以自动检测温度湿度等一系列环境情况并且可以自动做出相应的处理措施如简单的浇水和温度控制等&#xff0c;且数据情况可远程查看&#xff0c;以及用户可以实现远程控制。 基本实现原理 传感器通过串口将数据传递到Wi…

【蓝桥杯】每天一题,理解逻辑(1/90)【Leetcode 移动零】

文章目录 题目解析讲解算法原理【双指针算法思路】(数组下标充当指针)如何划分和执行过程大致 代码详情 题目解析 题目链接&#xff1a;https://leetcode.cn/problems/move-zeroes/description/ 题目意思解析 把所有的零移动到数组的末尾保持非零元素的相对顺序 理解了这两层…

DeepSeek R1满血+火山引擎详细教程

DeepSeek R1满血火山引擎详细教程 一、安装Cherry Studio。 Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。可以快速切换多个先进的 LLM 模型,提升工作学习效率。下载地址 https://cherry-ai.com/ 认准官网&#xff0c;无强制注册。 这…

【框架】参考 Spring Security 安全框架设计出,轻量化高可扩展的身份认证与授权架构

关键字&#xff1a;AOP、JWT、自定义注解、责任链模式 一、Spring Security Spring Security 想必大家并不陌生&#xff0c;是 Spring 家族里的一个安全框架&#xff0c;特别完善&#xff0c;但学习成本比较大&#xff0c;不少开发者都觉得&#xff0c;这个框架“很重” 他的…

Idea2024中搭建JavaFX开发环境并创建运行项目

Idea2024中搭建JavaFX开发环境并创建运行项目 本文以Java语言为例演示如何创建JavaFX开发项目和部署开发环境&#xff0c;读者可以根据个人实际灵活选择相关参数。 一、项目创建与环境搭建步骤 新建JavaFX项目&#xff0c;选择适合项目实际的语言、系统和JDK。 项目设置-设置…

Skyeye 云智能制造办公系统 VUE 版本 v3.15.10 发布

Skyeye 云智能制造&#xff0c;采用 Springboot winUI 的低代码平台、移动端采用 UNI-APP。包含 30 多个应用模块、50 多种电子流程&#xff0c;CRM、PM、ERP、MES、ADM、EHR、笔记、知识库、项目、门店、商城、财务、多班次考勤、薪资、招聘、云售后、论坛、公告、问卷、报表…

Solr中得Core和Collection的作用和关系

Solr中得Core和Collection的作用和关系 一&#xff0c; 总结 在Apache Solr中&#xff0c;Core和Collection 是两个核心概念&#xff0c;他们分别用于单机模式和分布式模式&#xff08;SolrCloud&#xff09;中&#xff0c;用于管理和组织数据。 二&#xff0c;Core 定义&am…

【2025-02-26】基础算法:二分查找(二)

&#x1f4dd;前言说明&#xff1a; ●本专栏主要记录本人的基础算法学习以及LeetCode刷题记录&#xff0c;主要跟随B站博主灵茶山的视频进行学习&#xff0c;专栏中的每一篇文章对应B站博主灵茶山的一个视频 ●题目主要为B站视频内涉及的题目以及B站视频中提到的“课后作业”。…

Cherry Studio 使用/训练deepseek

Cherry Studio前言 CherryStudio 是一款集多模型对话、知识库管理、AI 绘画、翻译等功能于一体的全能 AI 助手平台。 CherryStudio的高度自定义的设计、强大的扩展能力和友好的用户体验&#xff0c;使其成为专业用户和 AI 爱好者的理想选择。无论是零基础用户还是开发者&#…

十、大数据资源平台功能架构

一、大数据资源平台的功能架构图总体结构 大数据资源平台功能架构图 关键组件&#xff1a; 1.用户&#xff08;顶行&#xff09; 此部分标识与平台交互的各种利益相关者。 其中包括&#xff1a; 市领导 各部门分析师 区政府 外部组织 公民 开发人员 运营经理 2.功能模…

UE Python笔记

插件 官方 商城 Python Editorhttps://www.fab.com/listings/f4c99ba0-1a86-4f6a-b19d-2fd13f15961b GitHUB 好像只更新到了2020年4.2x的版本。可能有大佬改了5.x的版本。也希望分享给我一份。谢谢 https://github.com/20tab/UnrealEnginePython 学习笔记 网上教程一大堆。…

SQL_优化

1 SQL优化 (1) 数据读取 ①分区裁剪:使用时只读取需要的分区. ②列裁剪:读取操作(select、where、join、group by、sort by等),不读取不需要的列,减少IO消耗. (2) 数据筛选 ①分区先过滤,区分度大的字段先过滤. ②不在筛选字段上使用函数和表达式. (3) 分组聚合 ①使用窗口函数…

centos9之ESXi环境下安装

一、centos9简介 CentOS Stream 9是一个基于RHEL&#xff08;Red Hat Enterprise Linux&#xff09;的开源操作系统。它是CentOS Stream系列的最新版本。CentOS Stream是一个中间发行版&#xff0c;位于RHEL和Fedora之间&#xff0c;旨在提供更及时的软件更新和新功能。CentOS …