AVL树的模拟实现(c++)

news2025/4/12 3:39:35

目录

        搜索二叉树对于搜索查询来说是非常快的,但是它有着致命的缺陷,如果插入的数据是有序的,那么它的结构就会变成单链表,这对于搜索查询来说是非常不利的,因此为了解决搜索树的缺陷,弥补它的不足,引入了AVL树,它是由两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法 ,即:当向二叉搜索树中插入新节点后,如果能保证每个节点的左右子树的高度差的绝对值不超过1(需要对树中的节点进行调整),即可降低树的高度,从而减少平均搜索长度。

目录

1.AVL树的性质

2.AVL树节点的定义

        2.1AVL节点的定义 

       2.2 AVL树的定义

3.AVL树的插入

4.AVL树的旋转

5.AVL树的删除

6.AVL树的性能

7.其它代码


1.AVL树的性质

        1.它的左右子树都是AVL树

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

        如何实现AVL树呐,首先我们需要定义一个变量来记录每个节点的平衡因子,新插入的节点的平衡因子永远是 0,平衡因子等于当前节点右子树的高度减去左子树的高度。每次插入平衡因子后都需要对有关这个节点祖先的平衡因子进行更新,那么,什么时候更新结束呐,等节点的祖先的平衡因子等于零的时候更新结束,平衡因子等于0说明之前这颗子树的平衡因子是1或者-1,插入新的节点后树的高度没有变大,而是变得平衡了,此时就不需要更新平衡因子了,那么更新平衡因子的意义在哪里呢,如果更新新插入节点的平衡因子时,它的平衡因子变成-2或者2说明这棵子树不平衡了,需要调整了,此时不必更新平衡因子了,需要及时对这颗子树进行旋转。如图:

2.AVL树节点的定义

        2.1AVL节点的定义 

         需要注意的是这里使用了,三叉链,目的是方便平衡因子的更新。

    template<class K,class V>
	struct AVLTreeNode
	{
		AVLTreeNode<K,V> * _left;//左孩子
		AVLTreeNode<K, V>* _right;//右孩子
		AVLTreeNode<K, V> *_parent;//父亲节点
		int _bf;//平衡因子
		pair<K, V> _kv;
		AVLTreeNode(const pair<K,V> & kv)
			:_left(nullptr)
			,_right(nullptr)
			,_bf(0)
			,_parent(nullptr)
			,_kv(kv)
		{ }
	};

       2.2 AVL树的定义

	template<class K,class V>
	class AVLTree
	{
	public:
		typedef AVLTreeNode<K, V> Node;
		
	private:
		Node* _root;
	};

3.AVL树的插入

        AVL树的插入与二叉搜索树的插入一样,因此AVL树的插入分为两步:

        1.按照二叉搜索树的方式插入新节点

        2.调整节点的平衡因子 

      bool insert(const pair<K,V> &kv)
		{
			if (_root == nullptr)//第一次插入,没有节点首先给_root申请新的节点
			{
				_root = new Node(kv);
				return true;
			}
			
			Node* cur = _root;
			Node* parent = nullptr;
			//查找插入的位置
			while (cur)//从根节点开始
			{
				if (cur->_kv < kv)
				{
					parent = cur;//保存父节点
					cur = cur->_right;//走左边
				}
				else if(cur -> _kv > kv)
				{
					parent = cur;
					cur = cur->_left;//走右边
				}
				else
				{
					return false;//已经存在key值无法插入
				}
			}
			cur = new Node(kv);
			if (parent->_kv > kv)//对插入的位置进行判断
			{
				//插入到左边
				parent->_left = cur;
				cur->_parent = parent;
			}
			else
			{
				//插入到右边
				parent->_right = cur;
				cur->_parent = parent;
			}
			//更新平衡因子
			while (parent)
			{
				if (cur->_kv > parent->_kv)
				{
					//在右边平衡因子++
					parent->_bf++;
				}
				else if (cur->_kv < parent->_kv)
				{
					//在左边平衡因子--
					parent->_bf--;
				}

				if (parent->_bf == 0)
				{
					break;//平衡因子更新结束,无需调整
				}

				if (parent->_bf == -1 || parent->_bf == 1)
				{
					//继续更新平衡因子
					cur = parent;
					parent = parent->_parent;
				}
				else if (parent->_bf == -2 || parent->_bf == 2)
				{
					//此时parent所在子树已经不平衡了
					// 需要进行旋转处理
					//旋转parent所在的子树
					if (parent->_bf == 2)
					{
						if (cur->_bf == 1)
						{
							//左单旋
							rotateL(parent);
						}
						else if(cur->_bf == -1)
						{
							//右左双旋
							rotateRL(parent);
						}
					}
					else if(parent->_bf == -2)
					{
						if (cur->_bf == -1)
						{
							//右单旋
							rotateR(parent);
						}
						else if (cur->_bf == 1)
						{
							//左右双旋
							rotateLR(parent);
						}
					}
					
					
				}
				//旋转结束之后这棵树肯定是平衡的
				//直接结束就行
				break;
			}

		}

4.AVL树的旋转

        对于AVLtree的旋转首先要分成几种情况分开讨论,它的旋转还是有些复杂的。

       

        第一种:新节点插入较高左子树的左侧:右单旋 

        如图:

        图中这两种都是属于右旋的情况,这样的右旋情况的子树还有很多种,但是他们都有想同的特点此时parent的平衡因子是等于-2的。parent所在左子树的高度永远比右子树高2,所以我们可以对这种情况进行抽象,如下图: 

进行右单旋需要:将30的右子树b连接到60(parent) 的左孩子,将30的左子树连接到60(parent)处。需要注意的是这里使用的是三叉链,所以对于节点的parent指针也要进行更新。

        void rotateR(Node* parent)//右单旋
		{
			//保存需要改变的节点的关系
			Node* subL = parent->_left;
			Node*pParent = parent->_parent;
			Node* subLR = subL->_right;
			//更新节点的关系
			parent->_left = subLR;
			subL->_right = parent;
			//确保subLR存在
			if (subLR)
			{
				subLR->_parent = parent;
			}
			
			parent->_parent = subL;
			//判断parent的父节点是否是root节点,如果是root节点就要对_root节点进行更新
			if ( pParent == nullptr)
			{
				_root = subL;
				subL->_parent = nullptr;
			}
			else//对subL的父节点进行更新,更新之前需要确定parent与pParent的链接关系
			{
				if (pParent->_left == parent)
				{
					pParent->_left = subL;
				}
				else
				{
					pParent->_right = subL;
				}
				subL->_parent = pParent;

			}
			//旋转之后parent所在子树的高度会变低,所以subL和parent的平衡因子都会变为0

			subL->_bf = parent->_bf = 0;
		}

        第二种:新节点插入较高右子树的右侧:左单旋 

        如图: 

        具体情况和第一种类似,可以参考第一种。

	    void rotateL(Node* parent)//左单旋
		{
			//保存需要改变的节点的关系
			Node* subR = parent->_right;
			Node* subRL = subR->_left;
			Node* pParent = parent->_parent;
			parent->_right = subRL;
			subR->_left = parent;
			//确保subRL不为空
			if (subRL)
			{
				subRL->_parent = parent;
			}
			parent->_parent = subR;
			//判断parent的父节点是否为空,如果parent的父节点为空说明parent是_root节点,此时需要更新root节点
			if (pParent == nullptr)
			{
				_root = subR;
				subR->_parent = nullptr;
			}
			//对subR的父节点进行更新,更新之前需要确定parent与pParent的链接关系
			else
			{
				if (pParent->_left == parent)
				{
					pParent->_left = subR;
				}
				else
				{
					pParent->_right = subR;
				}
			}
			//旋转之后parent所在子树的高度会变低,所以subR和parent的平衡因子都会变为0

			subR->_bf = parent->_bf = 0;
		}

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

        如图: 

        需要注意的是涉及平衡因子的更新较为麻烦,需要根据具体情况进行更新。 

	    //左右双旋
		void rotateLR(Node* parent)
		{
			//记录需要进行左单旋和右单旋的子树节点
			Node* subL = parent->_left;
			Node* subLR = subL->_right;
			//对subLR的平衡因子进行记录,用来判断更新后的平衡因子
			int bf = subLR->_bf;
			rotateL(subL);//左单旋
			rotateR(parent);//右单旋
			//根据subLR的平衡因子对其他的平衡因子进行调节
			if (bf == -1)
			{
				subL->_bf = 0;
				subLR->_bf = 0;
				parent->_bf = -1;
			}
			else if (bf == 1)
			{
				subL->_bf = -1;
				parent->_bf = 0;
				subLR->_bf = 0;
			}
			else if (bf == 0)
			{
				subL->_bf = 0;
				subLR->_bf = 0;
				parent->_bf = 0;
			}
		} 

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

        如图: 

	    //右左双旋
		void rotateRL(Node* parent)
		{
			//记录需要进行左单旋和右单旋的子树节点

			Node* subR = parent->_right;
			Node* subRL = subR->_left;
			//对subRL的平衡因子进行记录,用来判断更新后的平衡因子
			int bf = subRL->_bf;
			rotateR(subR);
			rotateL(parent);
			//根据subRL的平衡因子对其他的平衡因子进行调节
			if (bf == -1)
			{
				parent->_bf = 0;
				subR->_bf = 1;
				subRL->_bf = 0;
			}
			else if (bf == 1)
			{
				parent->_bf = -1;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
			else if (bf == 0)
			{
				parent->_bf = 0;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
		}

        和第三种类似,具体参考第三种。 

5.AVL树的删除

         因为AVL也是二叉搜索树,可以按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与插入不同的是删除节点后的平衡因子更新,最差的情况一直要调整到根节点。

6.AVL树的性能

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

7.其它代码

         

        typedef AVLTreeNode<K, V> Node;
		AVLTree(const K& key = K(), const V& val = V())
		{
			//构造函数初始化根节点为空
			_root = nullptr;
		}
		~AVLTree()
		{
			//调用Destory()对节点进行释放
			Destory();
			_root = nullptr;
		}
		void _Destory(Node* root)
		{
			//对树进行后续遍历,并释放节点
			if (root == nullptr)
			{
				return;
			}
			_Destory(root->_left);
			_Destory(root->_right);
			delete root;
		}
		void Destory()
		{
			if (_root)
			{
				_Destory(_root);
			}
		}
        //查找key是否存在
		Node* Find(const K& key)
		{
			//根节点为空不需要查找
			if (_root == nullptr)
			{
				return nullptr;
			}
			else
			{
				//按照搜索树的方式进行查找
				Node* cur = _root;
				while (cur)
				{
					if (cur->_key == key)
					{
						//找到了返回节点的指针
						return cur;
					}
					else if (key > cur->_key)
					{
						cur = cur->_right;
					}
					else
					{
						cur = cur->_left;
					}
				}
				//走到这里说明cur为空,key不存在
				return cur;
			}
		}
        void _Inorder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_Inorder(root->_left);
			cout << root->_key << ":" << root->_val << endl;
			_Inorder(root->_right);
		}
		//对树按照递归的方式进行中序遍历
		void Inorder()
		{
			_Inorder(_root);
		}
		int _Hight(Node* root)
		{
			if (root == nullptr)
			{
				return 0;
			}
			int leftHight = _Hight(root->_left);
			int rightHight = _Hight(root->_right);

			return leftHight > rightHight ? leftHight + 1 : rightHight + 1;
		}
		//求树的高度
		int Hight()
		{
			return _Hight(_root);
		}
		bool _Balance(Node* root)
		{
			if (root == nullptr)
			{
				return true;
			}
			
			int leftHight = _Hight(root->_left);
			int rightHight = _Hight(root->_right);

			if (abs(leftHight - rightHight) > 1)
				return false;
			return (abs(leftHight - rightHight) > 1)
				&& _Balance(root->_left)
				&&_Balance(root->_right);
		}
		//判断树是否平衡
		bool Balance()
		{
			return _Balance(_root);
		}
		//判断是否是AVL树
		bool isAVLTree()
		{
			return _Balance(_root);
		}

        为什么这里对树进行中序遍历和其他操作的时候要写一个子函数呢,是因为_root是私有的成员,如果不使用子函数,在类的外面就不能调用了。 

        好咯,写的不好的地方还请指正批评,在下洗耳恭听。

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

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

相关文章

opencv实现仿射变换和透射变换

##1&#xff0c; 什么是仿射变换&#xff1f; 代码实现 import numpy as np import cv2 as cv import matplotlib.pyplot as plt#设置字体 from pylab import mpl mpl.rcParams[font.sans-serif] [SimHei]#图像的读取 img cv.imread("lena.png")#仿射变换 row…

loadEnv是vite的工具函数

loadEnv()函数返回一个对象&#xff0c;这个对象就是根据开发模式还是生产环境加载的.env.development文件里的环境变量&#xff0c;有系统自带的也有自己手写的 loadEnv(第1个参数&#xff0c;第2个参数&#xff0c;第3个参数) 注意&#xff1a;第3个参数如果是“”空字符…

一百八十六、大数据离线数仓完整流程——步骤五、在Hive的DWS层建动态分区表并动态加载数据

一、目的 经过6个月的奋斗&#xff0c;项目的离线数仓部分终于可以上线了&#xff0c;因此整理一下离线数仓的整个流程&#xff0c;既是大家提供一个案例经验&#xff0c;也是对自己近半年的工作进行一个总结。 二、数仓实施步骤 &#xff08;五&#xff09;步骤五、在Hive的…

华为孟晚舟:从最惨千金 到最强战士

作者&#xff1a;积溪 简评&#xff1a;华为25号开发布会&#xff0c;有何深意&#xff1f;从最惨千金到最强战士&#xff0c;孟晚舟和华为都回来了 #华为发布会 #孟晚舟 #任正非 #华为 华为发布会 在打谁的脸&#xff1f; 苹果只是前菜 今天才是正餐 两年前的今天 华为…

Vue3最佳实践 第五章 Vue 组件应用 2 ( Emit )

本章带领大家理解组件、props、emits、slots、providers/injects&#xff0c;Vue 插件 等Vue组件使用的基础知识。 第一章 Vue3项目创建 1 Vue CLI 创建vue项目 第一章 Vue3项目创建 2 使用 Webpack 5 搭建 vue项目 第一章 Vue3项目创建 3 Vite 创建 vue项目 第二章 Vue3 基础语…

Latex math equation中如何不斜体

math equation的字母会斜体&#xff0c;只需给不想斜体的字母加上 \text{NPSB}

C/C++鸡尾酒疗法 2023年5月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析

目录 C/C鸡尾酒疗法 一、题目要求 1、编程实现 2、输入输出 二、解题思路 1、案例分析 三、程序代码 四、程序说明 五、运行结果 六、考点分析 C/C鸡尾酒疗法 2020年6月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 鸡尾酒疗法&#xff0c;原指“高效抗…

分治算法求解:逆序对,Max Sum,棋盘覆盖,a-Good String——中山大学软件工程学院算法第四次实验课 必做+选做题

写英文注释不是要“秀英文”&#xff0c;而是因为鄙人正在准备雅思&#xff0c;顺手练习 逆序对 题目描述 完整代码 #include<iostream> using namespace std; int num[500010]; // input numbers int tmp[500010]; // sequence after merging left and right part lon…

【李沐深度学习笔记】线性回归

课程地址和说明 线性回归p1 本系列文章是我学习李沐老师深度学习系列课程的学习笔记&#xff0c;可能会对李沐老师上课没讲到的进行补充。 线性回归 如何在美国买房&#xff08;经典买房预测问题&#xff09; 一个简化的模型 线性模型 其中&#xff0c; x → [ x 1 , x 2 ,…

如何精细化管理APP用户生命周期,寻找业绩增长点?

在APP精细化运营中&#xff0c;经常会提到用户生命周期&#xff0c;在对APP进行运营的时候&#xff0c;需要明确&#xff0c;自己的APP是处于产品生命周期的哪一个&#xff0c;然后根据这个生命周期的特点&#xff0c;使用最准确的运营方法。 01、为什么要提升用户生命周期价值…

杭州亚运会吉祥物制作,能给城市3D虚拟数字人定制带来什么启发?

继冬奥顶流“冰墩墩”后&#xff0c;吉祥物“江南忆”作为杭州亚运会吉祥物也火爆出圈&#xff01;从北京亚运会的熊猫“盼盼”、到广州亚运会“五羊”再到如今的杭州亚运会吉祥物“宸宸、琮琮、莲莲”&#xff0c;这些吉祥物凭借其形象火爆出圈&#xff0c;能给城市3D虚拟数字…

腾讯mini项目-【指标监控服务重构】2023-08-25

今日已办 traefik proxy jaeger Prometheus prometheus | Prometheus 配置完依然无法实现 web-url的前缀访问【待解决】 Set span storage type : elasticsearch services:elasticsearch:image: elasticsearch:7.17.12container_name: elasticsearchnetworks:- backend # …

String的增删查【C++】

String的增删查【C】 前言string的增删查改构造与析构构造string(const char* str "")赋值构造string(const string& s1) 赋值重载析构函数增reservepush_backappendinsert 删erase 查迭代器流插入流提取流插入流提取 前言 从这里开始可以算是进入了STL的学习中…

火山引擎DataLeap推出两款大模型应用: 对话式检索与开发 打破代码语言屏障

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 自上世50年代&#xff0c;以“计算机”作为代表性象征的信息革命开始&#xff0c;社会对于先进生产力的认知便开始逐步更迭——从信息化&#xff08;通常认为是把企…

kafka latest 模式消费偏移丢数据

Flink消费kafka&#xff0c;这种情况会丢数据

Vue.js 2 —组件(Component)化编程

一、模块与组件 模块 1. 理解 : 向外提供特定功能的 js 程序, 一般就是一个 js 文件 2. 为什么 : js 文件很多&#xff0c;很复杂 3. 作用 : 复用 js, 简化 js 的编写, 提高 js 运行效率 组件 组件是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素&#xff0c;封装…

在呼叫中心领域,人工智能目前处在什么阶段

在呼叫中心这个行业&#xff0c;人工智能已经逐渐走向实用化阶段。目前&#xff0c;很多企业已经开始采用人工智能技术来改善其呼叫中心的效率和服务质量。 具体来说&#xff0c;人工智能已经被用于呼叫中心自动语音应答、自然语言处理、智能路由、智能客服机器人等方面。通过这…

机器学习中的分类问题:如何选择和理解性能衡量标准

文章目录 &#x1f34b;引言&#x1f34b;为什么需要分类问题的性能衡量标准&#xff1f;&#x1f34b;常用的分类问题衡量标准&#x1f34b;混淆矩阵-精确率-召回率&#x1f34b;PR曲线和ROC曲线&#x1f34b;PR曲线&#x1f34b;ROC曲线&#x1f34b;PR vs. ROC &#x1f34b…

外汇天眼:外汇新手开展交易需要做哪些准备,你都知道么?

外汇交易&#xff0c;如同任何一项专业工作&#xff0c;需要不断积累知识和经验&#xff0c;以及稳定的心态。正如古语所说&#xff1a;“工欲善其事&#xff0c;必先利其器。” 在外汇市场&#xff0c;这句话同样适用。在踏上外汇交易之旅之前&#xff0c;我们迫切需要做好外汇…

Windows清除激活标志的方法

大家在购买电脑或笔记本的时候&#xff0c;有的商家给出的7天无理由退货&#xff0c;并不是真正的无理由&#xff0c;往往附件条件windows是不能激活的&#xff0c;如果激活了就只能换不能退了。 卖家提出的条件也特别滑稽可笑&#xff0c;你想不联网怎么体验啊&#xff1f;不…