AVL树的插入详解

AVL树 

为什么有AVL树的出呢?其实我们用的map/multimap/set/multiset的底层都是二叉搜索树,但是二叉搜索树有一个很大的缺陷,就是当往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。也就是AVL树和红黑树。这篇内容先介绍红黑树的实现。


概念

正是由于二叉搜索树的缺陷所以先有了AVL树,而AVL树的特征如下:

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

所以说此时就可以保证AVL树的查找效率为:O(n*log2n) 

AVL树的节点定义

    template<class K,class V>
	struct AVLTreeNode
	{
		AVLTreeNode(const pair<K, V>& kv)
			:_left(nullptr)
			,_right(nullptr)
			,_parent(nullptr)
			,_kv(kv)//pair内部有实现自己的拷贝构造函数
			,_bf(0)
		{}

		AVLTreeNode<K,V>* _left;
		AVLTreeNode<K, V>* _right;
		AVLTreeNode<K, V>* _parent;

		pair<K, V> _kv;
		
		int _bf;//每个节点的平衡因子(right-left)
	};

    template<class K, class V>
	class AVL
	{
		typedef AVLTreeNode<K,V> Node;
	public:
		AVL()
		{}


    private:
		Node* _root = nullptr;

	};

首先我们要知道AVL树的数据类型一般都是K-V型,具体说也就是pair类型,而且AVL树和一般的树据关联是不同的,属于三叉连,也就是不仅仅要有左右指针还要有父指针,而父指针的目的就是在插入数据的时候可以更方便的处理祖宗节点的平衡因子。平衡因子的计算:右子树的高度—左子树的高度。

AVL树的插入

我们知道AVL树的实质其实也是优化后的的二叉搜索树,所以插入数据是比较容易的,核心也就是比较数据大小然后插入,但是不要忘了记录父节点的位置:

        bool insert(const pair<K,V>& kv)
		{
            //先判断是否为空树
			Node* newroot = new Node(kv);
			if (_root == nullptr)
			{
				_root = newroot;
				return true;
			}
			Node* parent = _root, *cur = _root;

			//插入数据
			while(1)
			{
				parent = cur;
				if (cur->_kv.first > kv.first)
				{
					cur = cur->_left;
					if (cur==nullptr)
					{
						parent->_left = newroot;
						newroot->_parent = parent;
						break;
					}
				}
				else if (cur->_kv.first < kv.first)
				{
					cur = cur->_right;
					if (cur==nullptr)
					{
						parent->_right = newroot;
						newroot->_parent = parent;
						break;
					}
				}
				else
				{
					return false;
				}

			}
        }

其实就仅仅插入数据来说的话,其实AVL树是挺简单的,但是真正的重头戏其实是处理平衡因子,因为平衡因子的值是有条件的:绝对值小于一,所以一般的数据插入对AVL树而言是行不通的,后续还需要对树的节点进行处理,使其平衡。所以接下来就有两步:

  1. 处理平衡因子的值
  2. 使不满足条件的二叉树进行调整成符合要求的AVL树        

平衡因子的值处理

对于平衡因子的值处理是比较轻松的,就是对于插入节点而言,若在其父节点的右边插入的话,父节点的平衡因子就+1,相反如果在左边插入的话,就将父节点的平衡因子-1,但是还没完,插入节点可能不仅仅只影响其父节点也可能会影响到插入节点的祖先节点。所以我们就要开始对插入数据之后的父节点平衡因子进行分析:

父节点的平衡因子变为0

原来父节点平衡因子是1或-1,

如上是在右边进行插入数据,所以此时父节点的平衡因子变为0,但是该子树的高度并没有发生改变,所以此时的插入情况并不会影响除父节点外祖宗节点的平衡因子

父节点的平衡因子变为1或-1

原来父节点的平衡因子为0,

所以此时的插入会使得该子树的高度+1,所以此时就会影响除父节点外祖宗节点的平衡因子所以此时对父节点(2就是父节点)的影响就取决于该树是在其父父节点的左子树还是右子树。若在其父父节点的右边的话,父父节点的平衡因子就+1,相反如果在左边的话,就将父父节点的平衡因子-1,此时循环下去,直到达到树的根节点。

父节点的平衡因子变为2或-2也就是左右子树高度差为2,所以此时证明父节点在插入数据之前的平衡因子是1或-1,而不可能会是0(假设法)所以此时就要将该树进行调整使其满足AVL树的特性。

插入数据之后平衡因子只有以上三种可能的情况,因为当平衡因子为2或-2的时候就要进行调整的,而调整之后的平衡因子肯定不再是2或-2,所以如果平衡因子出现其他值的话就证明实现出了问题。

使不满足条件的二叉树进行调整成符合要求的AVL树        

其实我们就以先普通再一般的方式来研究树的半边,而另一半边就相当于这半边翻转过来的,所以此时研究一半就行。

就拿最简单的情况来说,当平衡因子而2时,那么此时就要开始调整树的走向了,使其能够满足AVL树的条件。

  • 就拿左边这个图来说,当我们插入数据11时,7节点所在节点的平衡因子就要更新,此时会一直更新到根节点的位置处,但是此时发现根节点的平衡因子不满足条件,所以此时就要对该节点进行处理,使其平衡因子满足条件,此时几乎可以瞬间就想到:将7节点作为新的根节点,4节点作为7节点的左子树,此时不仅仅满足二叉搜索树,而且最主要的是插入之前该树根节点的平衡因子是1,而再插入调整之后,该树的平衡因子还是1,而该树也可能是其他节点的子树,所以调整之后也不用继续调整祖先节点的平衡因子,只需要调整这两个节点的平衡因子即可。而对于这种调整我们称为左单旋。
  • 而有了第一种类型树的调整方式,那么对于第二棵树的调整方式就可以以此参考,对于第二种情况下,对于该树的调整方法如果采用和第一种一样的方法话,那就是进行因此左旋,而为了满足搜索二叉树的特性,就将7节点的左子树给4节点的右子树,所以此时会发现然而并没有发挥任何的作用,只不过将根节点的平衡因子从2变成-2了而已。也就是如下图:

 所以我们就可以换一种方式,先观察左右两幅图左图中属于连续一边插入,对应的平衡因子的符号是同号的,也就是说如果都是正的,则采用左单旋,那如果都是负的话就采用右单旋。但是右图的平衡因子的符号是异号的,所以仅仅通过一个单旋是无法解决的,此时我们就可以考虑将右图先进行右单旋,使其结构与左图的结构相同,然后再采用左单旋进行操作即可。而对于三个节点而言各自的平衡因子都是0.

以上皆属于对具体情况的分析,而实际上树的节点为2或-2时的情况是有很多种的,所以还是得进行抽象的分析。

左右单旋

左右单旋的情况其实就是针对直线型插入的情况,所以此时就有两种解决方案:左单旋、右单旋。

以右单旋的情况为例:

 

此时其实就是需要系那个30节点当作新的根,60节点当作30节点的右子树,b这个树给60节点的左边,此时就算是完成了。

而对于平衡因子而言,我们只需要知道一点,一个节点的平衡因子与该节点的左右子树高度差有关,所以说此时我们只需要判断节点的高度差就行了。而且AVL树的插入流程是先判断在哪里插入,然后在一步步的向上移动,直到节点的平衡因子为2或-2的时候进行调整处理,所以此时我们需要处理平衡因子的节点也就只有60和30两个节点,因为这两个节点的左右子树的位置是发生了改变,而且都为0。而且最重要的是插入数据之前该树高度和调整之后该树的高度是没有发生变化的,所以也不用再继续向上去调整节点平衡因子的值。


而且最容易忽视的就是将该树调整完了以后就直接跳出循环,其实我们还要判断该树的根节点是否有父节点(一定要提前存好,后面调整完之后指针指向会发生改变),如果有的话就要将该树新的根节点和其原来的父节点链接起来,如果没有的话就要将AVL树的根节点重置成调整后的新根节点。

单旋代码示例 

        void levorotation(Node* parent)//左单旋(_bf=2)
		{
			Node* childr = parent->_right;
			Node* childrl = childr->_left;
			Node* pparent = parent->_parent;//存父节点

			//链接起来
			childr->_left = parent;
			parent->_parent = childr;
			parent->_right = childrl;
			if(childrl)//有可能该节点为空
				childrl->_parent = parent;
			
			//链接父节点
			if (parent == _root)//处理父节点指向
			{
				_root = childr;
				childr->_parent = nullptr;
			}
			else
			{
				if (pparent->_right == parent)
					pparent->_right = childr;
				else
					pparent->_left = childr;

				childr->_parent = pparent;
			}

			//处理平衡因子
			parent->_bf = childr->_bf = 0;//
		}

		void dextrorotation(Node* parent)//右单旋(_bf=-2)
		{
			Node* childl = parent->_left;
			Node* childlr = childl->_right;
			Node* pparent = parent->_parent;;

			childl->_right = parent;
			parent->_parent = childl;
			parent->_left = childlr;
			if (childlr)
				childlr->_parent = parent;
			
			if (_root == parent)
			{
				_root = childl;
				childl->_parent = nullptr;
			}
			else
			{
				if (pparent->_left == parent)
					pparent->_left = childl;
				else
					pparent->_right = childl;

				childl->_parent = pparent;
			}
				
			//处理平衡因子
			parent->_bf = childl->_bf = 0;//
		}

实现代码时,需要我们注意的是:三叉链结构别忘了父指针的调整,以及对于空指针的访问,所以具体实现的过程中还是有很多的细节。

左右双旋

左右双旋相较于单旋而言其实就是double,而需要左右单旋的情况我们一般可以称为折线型插入,所以说我们也是先拿抽象图进行具体分析:

对于双旋的情况其实 真正复杂的点就是平衡因子的变化,就上图而言,我们知道当在60节点的下方插入数据时就需要采用双旋来解决。而对于插入数据又有两种情况:

不同的情况下数据所对应的平衡因子是不相同的,也就是在插入数据以后60这个节点的平衡因子会有两种可能,也就是表明对于不同的插入,调整之后的平衡因子可能不一样:

所以说我们双旋之后的平衡因子的更改就可以依据60这个节点的平衡因子是正还是负,而且在调整之后不难发现60节点成为了新的根节点,60节点的左子树变成了30节点的右子树,而60节点的右子树变成了90节点的左子树。

而且在插入数据之后调整平衡因子的过程最终并没有影响该树的高度所以也不会影响该该树的父节点的平衡因子。


而且最容易出错的是代码写的太过冗余,我们一定要将90,60,30这三个节点先存起来,因为我们双旋其实就是复用两次单旋的函数而实现的,所以说在单旋完成之后父亲“”还是不是“父亲”就真的说不准了,所以最后调整平衡因子的时候就可以直接调整。

 双旋代码示例

                else//parent->_bf=-2或2则需要旋转
				{
					//原来parent->_bf为-1或者1
					if (parent->_bf == 2 && newroot->_bf == 1)//左单旋
						levorotation(parent);
					else if (parent->_bf == -2 && newroot->_bf == -1)//右单旋
						dextrorotation(parent);
					else if (parent->_bf == 2 && newroot->_bf == -1)//
					{
						Node* newrootl = newroot->_left;//记录下来该节点

						//先右单旋再左单旋
						int bf = newrootl->_bf;//记录插入节点的位置
						dextrorotation(newroot);
						levorotation(parent);
						
						//旋转以后节点以及换过位置newroot->_left变成新的父节点
						if (bf == 0)
						{
							parent->_bf = newroot->_bf = newrootl->_bf = 0;
						}
						else if (bf == 1)
						{
							newrootl->_bf = 0;
							parent->_bf = -1;
							newroot->_bf = 0;
						}
						else if(bf==-1)
						{
							newrootl->_bf = 0;
							parent->_bf = 0;
							newroot->_bf = 1;
						}
							
					}
					else if (parent->_bf == -2 && newroot->_bf == 1)
					{
						Node* newrootr = newroot->_right;//记录下来该节点(旋转的是会改变节点指向)

						//同理
						int bf = newrootr->_bf;
						levorotation(newroot);
						dextrorotation(parent);

						parent->_bf = 0;
						if (bf == 0)
							newrootr->_bf = parent->_bf = newroot->_bf = 0;
						else if (bf == 1)
						{
							newrootr->_bf = 0;
							parent->_bf = 0;
							newroot->_bf = -1;
						}
						else if (bf == -1)
						{
							newrootr->_bf = 0;
							parent->_bf = 1;
							newroot->_bf = 0;
						}
					}
					return true;
				}

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

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

相关文章

计算机服务器中了locked勒索病毒怎么办,勒索病毒解密,数据恢复

随着网络技术的不断成熟&#xff0c;网络中存在的病毒威胁也不断增多&#xff0c;近期&#xff0c;云天数据恢复中心陆续接到很多企业的求助&#xff0c;企业的计算机服务器数据库遭到了勒索病毒攻击&#xff0c;并且勒索病毒的攻击与加密形式也发生了许多变化。其中攻击次数较…

记一次 Android 周期性句柄泄漏的排查

滴滴国际化外卖 Android 商户端正常迭代版本过程中&#xff0c;新版本发布并且线上稳定一段时间后&#xff0c;突然触发线上 Crash 报警。 第一次排查发现是在依赖的底层平台 so 库中崩溃&#xff0c;经过沟通了解到其之前也存在过崩溃问题&#xff0c;所以升级相关底层 so 版本…

Linux mx6ull-驱动(1)hello

编写第一个驱动&#xff0c;hello_drv 一、获取内核、编译内核。 这里为什么要获取内核呢&#xff0c;因为我们写的是驱动程序&#xff0c;而不是裸机程序。也就是我们的板子已经烧入进去了uboot、内核&#xff0c;根文件。然后我们要在这个板子的内核的基础上&#xff0c;来…

【uniapp】签名组件,兼容vue2vue3

网上找了个源码改吧改吧&#xff0c;清除了没用的功能和兼容性&#xff0c;基于uniapp开发的 样子 vue2 使用方法&#xff0c;具体的可以根据业务自行修改 <signature ref"signature" width"100%" height"410rpx"></signature>confi…

[鹏程杯2023]复现

SecretShare X的20个值和R的21个值已经被全部泄露&#xff0c;X和R都是1024bit的值&#xff0c;此时X总共泄露了32*20 640&#xff0c;于是&#xff0c;此时我们可以使用mt19937将其还原&#xff0c;还原之后&#xff0c;我们往前推20个1024bit的值&#xff0c;便可以求得A的…

C语言 预处理详解

目录 1.预定义符号 2.#define 2.1#define 定义标识符 2.2#define 定义宏 2.3#define 替换规则 2.4#和## 2.4.1# 的作用 2.4.2## 的作用 2.5 带有副作用的宏参数 2.6宏和函数的对比 对比 **2.7内联函数 2.8命名约定 3.#undef **4.命令行定义 5.条件编译 常…

AVL树性质和实现

AVL树 AVL是两名俄罗斯数学家的名字&#xff0c;以此纪念 与二叉搜索树的区别 AVL树在二叉搜索树的基础上增加了新的限制&#xff1a;需要时刻保证每个树中每个结点的左右子树高度之差的绝对值不超过1 因此&#xff0c;当向树中插入新结点后&#xff0c;即可降低树的高度&…

闪客网盘系统源码,已测试对接腾讯COS及本地和支付(支持限速+按时收费+文件分享+可对接易支付)- 修复版

正文概述 资源入口 支持对文件下载限速 对接易支付 推广赚钱啥的功能 源码非常的好 支持腾讯cos 阿里云cos 本地储存 远程存储 源码仅支持服务器搭建 php7.2 伪静态thinkphp 运行目录public 导入数据库 修改config目录下的database.php数据库信息 后台地址&#xff1a; 域名/ad…

如何在CentOS上安装SQL Server数据库并通过内网穿透工具实现远程访问

文章目录 前言1. 安装sql server2. 局域网测试连接3. 安装cpolar内网穿透4. 将sqlserver映射到公网5. 公网远程连接6.固定连接公网地址7.使用固定公网地址连接 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;…

CLIP Surgery论文阅读

CLIP Surgery for Better Explainability with Enhancement in Open-Vocabulary Tasks&#xff08;CVPR2023&#xff09; M norm ⁡ ( resize ⁡ ( reshape ⁡ ( F i ˉ ∥ F i ‾ ∥ 2 ⋅ ( F t ∥ F t ‾ ∥ 2 ) ⊤ ) ) ) M\operatorname{norm}\left(\operatorname{resize}\…

文件包含 [ZJCTF 2019]NiZhuanSiWei1

打开题目 代码审计 if(isset($text)&&(file_get_contents($text,r)"welcome to the zjctf")){ 首先isset函数检查text参数是否存在且不为空 用file_get_contents函数读取text制定的文件内容并与welcome to the zjctf进行强比较 echo "<br><h…

【C++】类型转换【4中类型转换】

目录 1. C语言中的类型转换 2. C的四种类型转换 2.1 static_cast 3.2 reinterpret_cast 3.3 const_cast 3.4 dynamic_cast 3. explict 4. RTTI&#xff08;了解&#xff09; 1. C语言中的类型转换 在 C 语言中&#xff0c;如果 赋值运算符左右两侧类型不同&#xff0…

如何用Excel做最小二乘法②

因为在Excel里面做最小二乘法是需要用到LINEST函数的&#xff0c;所以如果不知道怎么对数据进行最小二乘法时&#xff0c;就应该研究一下LINEST函数。 LINEST 函数语法 LINEST(known_ys, [known_xs], [const], [stats]) known_ys (必须) 因变量&#xff0c;单行/单列known_xs…

xxl-job 原理

一、xxl-job 架构设计 总体分两个部分&#xff1a; 调度中心&#xff1a;负责管理调度信息&#xff0c;按照调度配置发出调度请求&#xff0c;自身不承担业务代码。调度系统和任务解耦&#xff0c;提高了系统可用性和稳定性。通调度性能不在受限于任务模块。执行器&#xff1a…

Mysql配置主从复制-GTID模式

目录 主从复制 主从复制的定义 主从复制的原理 主从复制的优势 主从复制的形式 主从复制的模式 主从复制的类型 GTID模式 GTID的概念 GTID的优势 GTID的原理 GTID的配置 Mysql主服务器 ​编辑 Mysql从服务器 ​编辑 主从复制 主从复制的定义 是指把数据从一个…

Docker安装Mysql

Docker常用指令&#xff1a; docker search 镜像名&#xff1a;寻找镜像 docker pull 镜像名&#xff1a;拉去镜像 docker images :查看拥有镜像 docker ps :查看正在运行容器 docker pa -a &#xff1a;查看所有容器&#xff08;包含运行中的和停止的&#xff09; dock…

案例 - 拖拽上传文件,生成缩略图

直接看效果 实现代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>拖拽上传文件</title>&l…

Python 实现动态动画心形图

在抖音上刷到其他人用 matlab 实现的一个动态心形图&#xff0c;就想也用 Python 实现&#xff0c;摸索了两种实现方式&#xff0c;效果如下&#xff1a; 方法一&#xff1a; 利用循环&#xff0c;结合 pyplot 的 pause 与 clf 参数实现图像的动态刷新 import matplotlib.p…

【漏洞复现】weblogic-10.3.6-‘wls-wsat‘-XMLDecoder反序列化(CVE-2017-10271)

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞扫描nacsweblogicScanner3、漏洞验证 说明内容漏洞编号CVE-2017-10271漏洞名称Weblogic < 10.3.…

【Unity细节】VS不能附加到Unity程序中解决方法大全

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 &#x1f636;‍&#x1f32b;️收录于专栏&#xff1a;unity细节和bug &#x1f636;‍&#x1f32b;️优质专栏 ⭐【…