C++——AVL树的模拟实现

news2024/10/9 5:53:01

目录

一、AVL树结点

二、AVL树结构

三、插入数据(重点)

        1、右单旋

        2、左单旋

        3、左右双旋

        4、右左双旋


         AVL树是一颗平衡二叉搜索树,它的本质就是一颗之前说过的二叉搜索树。但是二叉搜索树可能会出现极端情况,导致二叉搜索树变成一颗单边的树,这样使用二叉搜索树的效率就大大降低了,为了防止这种情况的出现,有了新的平衡二叉搜索树。它就是在普通二叉树的基础上多了一个要求——所有结点的左右子树的最大高度差不能超过 2。这样不仅能防止出现普通二叉搜索树中的极端情况,还能有效降低树的高度,减少搜索需要的时间消耗,同时构建时花费的时间也不是很多。下图就是一颗平衡二叉搜索树(平衡因子我们采用右-左的形式)。

        

一、AVL树结点

        平衡二叉搜索树的结点和二叉搜索树的结点结构是极为类似的,为了方便维护这个二叉搜索树,我们给结点添加了两个新的成员变量_bf表示当前结点的平衡因子,如果平衡因子不符合要求,我们需要对结点进行翻转操作。同时为了方便翻转操作,我们添加了一个_parent指针指向父亲结点。

template<class T>
struct AVLTreeNode
{

	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	T _data;
	int _bf;

	AVLTreeNode(const T& data=T())
		:_data(data)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{
	}
};

二、AVL树结构

        和所有的树型结构一样,我们只需要通过一个根节点的指针就能找到这棵树,所以在AVL树的结构里我们也只需要一个指向根节点的指针这样的成员变量。

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;

private:
	Node* _root = nullptr;
};
 

三、插入数据(重点)

        插入数据的过程分为两步,第一步就是找到要插入的数据的位置,这一步和之前在二叉搜索树里讲的是一样的,如果当前位置的值比插入数据大,则左子树里继续寻找,若比当前位置小,则在右子树里继续寻找,直到找到应当插入的位置,把数据插入进去;如果在树里已经存在数据,则插入失败。

        插入数据的第一步是完全重复的,但是我们要保证在插入数据以后,这个二叉树还能保持平衡,此时我们就要从插入结点的位置向上开始更新平衡因子。

        假如我们在左子树中插入了数据,此时我们可能影响的结点最多只会到根结点,并且是沿着插入位置一直向上的,与根结点的右子树是完全无关的,所以我们只要沿着插入的位置寻找父亲结点更新平衡因子即可。同时我们可以通过判断插入位置在父亲结点的左孩子还是右孩子,即可判断平衡因子是加一还是减一(平衡因子这里采用右-左的形式,所以如果插入在右孩子父亲的平衡因子加1,插入在左孩子,父亲的平衡因子减一)。

        如果更新了父亲的平衡因子以后,发现父亲的平衡因子变成了0,那么父亲的平衡因子一定是由-1>>0或者1>>0。这说明在插入新结点之前,父亲结点的左右子树处于一个一边高一边低的状态,同时我们的新结点插入到了低的那一边,因此结点所在子树的高度并没有发生改变,所以插入新的结点的时候只会影响到这个父亲结点的位置,再往上的结点的平衡因子不会发生改变,所以可以退出循环。

        如果更新了父亲的平衡因子以后,发现父亲的平衡因子变成了1或者-1,那么父亲的平衡因子一定是由0>>1或者0>>-1,这说明在插入新结点之前父亲结点所在子树的左右子树的高度是一样,在插入新的结点以后,变成了一高一低的情况,父亲结点所在子树的高度发生了变化,因为父亲的父亲结点的平衡因子要发生改变,所以还要继续向上更新。

        如果更新了父亲的平衡因子以后,发现父亲的平衡因子变成了2或者-2,那么此时平衡二叉搜索树的平衡就被打破了,我们在当前结点进行翻转操作来降低树的高度,达到让这个树重新恢复平衡的目的,这个旋转又分为四种情况——右单旋、左单旋、左右双旋、右左双旋,这个是AVL树中最重要的部分,也是最难的部分。

	bool Insert(const T& data)
	{ 
		if (_root == nullptr)
		{
			_root = new Node(data);
			return true;
		}

		//找插入的位置
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
				assert(false);
		}

		cur = new Node(data);
		if (parent->_data > data)
			parent->_left = cur;
		else
			parent->_right = cur;
		cur->_parent = parent;

		//更新平衡因子
        //根结点的父亲是nullptr 所以更新到根结点的时候就会停下来
		while (parent)
		{
            //判断新插入的结点在父亲的左孩子还是右孩子
			if (cur == parent->_left)
				parent->_bf--;
			else
				parent->_bf++;

            //通过父亲结点判断是否需要继续更新
			if (parent->_bf == 0)
				break;
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//旋转

				break;
			}
			else
				assert(false);
		}

		return true;
	}

        1、右单旋

        这里我们拿这样的一颗树来举例,这里的10这个结点可以是整颗树的根结点,也可以是整颗树中一个子树的根结点,而a、b、c三棵子树则是高度为h(h>=0)的三棵树,这样抽象出来的一棵树就能包括右单旋的所有情况。

        这时是在a的左子树或者右子树中插入了一个数据,使得a子树的高度变成了h+1,此时5的平衡因子由0变成了-1.继续向上更新平衡因子;10的平衡因子由-1变成了-2,此时以10为根结点的子树的平衡被破坏了,在这个结构下,我们要对以10为根结点的子树进行右单旋,使其高度由h+2恢复成h+1。

        5的右子树b中的所有值是大于5并且小于10的,所以我们可以把b子树接到10的左子树上是不会破坏二叉搜索树的结构的,然后我们再把10接到5的右子树上,我们会发现以10为根高度为h+2的子树变成了以5为根高度为h+1的子树,同时所有结点也都符合二叉搜索树的规则;并且在插入新结点之前以10为根的子树的高度也是h+1,所以就不用继续向上更新平衡因子了。

        在旋转的理论环节我们只需要改变子树的连接位置即可,但是在实际的代码实现中我们还要同步的改变_parent指针的指向,同时还要注意h的高度为0时的情况。

        我们可以先用两个变量subL(结点5)和subLR(b子树的根结点)来标记会经常使用到的这两个子树的根结点。第一步就是把subLR接到parent(10结点)的左边,当h为0时,subLR就是空指针,就不用把subLR的_parent指针接回去,不然会造成访问空指针的错误;第二步要保存子树根结点父亲的信息,因为parent结点可能是整棵树的根结点,也有可能是一部分子树的根结点,我们在旋转完了以后,要把新的根结点接到原先的位置上;第三步就是把parent接到subL的右子树上;第四步就是把新的根结点subL接到原先的位置上,如果parent为整棵树的根结点,那么它是没有父亲结点的,此时要更新整个AVL树_root的指向,如果parent只是一部分子树的根,那就把subL接回原先的位置即可;最后一步就是更新平衡因子,从图中我们可以很清楚的看出来,在完成旋转以后a子树的平衡因子没有发生改变,但是subL和parent的左右子树的高度差都变成了0,所以我们在这里要把他们的平衡因子更新一下。

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

		parent->_left = subLR;
        //h为0时就不用再把subLR的_parent接回去
		if (subLR)
			subLR->_parent = parent;

        //保存子树根结点的父亲信息
		Node* pParent = parent->_parent;

        //把parent接到subL的右边
		subL->_right = parent;
		parent->_parent = subL;
        
        //把subL接到原先parent的位置
		subL->_parent = pParent;
		if (pParent == nullptr)
            _root = subL;
		else
		{
			if (pParent->_left == parent)
				pParent->_left = subL;
			else
				pParent->_right = subL;
		}

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

        2、左单旋

        左单旋的情况和右单旋是完全类似的,这里拿下图举例,就是在15的右子树中插入一个结点,导致15的右子树a由高度h变成了h+1,此时15的平衡因子为1,10的平衡因子为2,二叉搜索树的平衡被破坏,所以要在10的位置进行左单旋。

        左单旋的步骤就是先让15的左子树b接到10的右子树上,再让10变成15的左子树,最后再把15接回原来的位置即可,这里要注意的事项和右单旋也是一样的,右h为0以及parent是整个树的根的可能性。

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

        //把subRL接到parent的右边
		parent->_right = subRL;
        //h为0时subRL为空树
		if (subRL)
			subRL->_parent = parent;

        //记录parent的父亲结点的信息
		Node* pParent = parent->_parent;

        //把parent接到subR的左边
		subR->_left = parent;
		parent->_parent = subR;

        //把subR接到原先的父亲下面
		subR->_parent = pParent;
        //判断原本的parent是否为整棵树的根结点
		if (pParent == nullptr)
			_root = subR;
		else
		{
			if (parent == pParent->_left)
				pParent->_left = subR;
			else
				pParent->_right = subR;
		}
		
        //更新平衡因子
		parent->_bf = subR->_bf = 0;
	}

        3、左右双旋

        左右双旋的初始模型和右单旋的模型相同,新增的结点不是在a而是在b上,此时就需要用到左右双旋。

        

        左右双旋就和它的名字一样,只需要先进行一次左单旋,再进行一次右单旋就行,左右双旋的要点在于平衡因子的更新,要分三种情况分别讨论。

        左右双旋的操作就是先对parent的左子树的根节点进行一次左单旋,再对parent为根结点的子树进行一次右单旋就可以实现重新平衡的目的,但是这样做平衡因子并没有完全更新好,所以我们要分三种情况讨论平衡因子的更新情况。

        一:插入的结点在subLR的左子树,因为中间过程中需要更新平衡因子的只有parent、subL、subLR三个结点,所以这里我们可以通过观察最后的结果来更新平衡因子,当插入的位置在subLR的左子树时,subL的平衡因子为0,subLR的平衡因子为0,parent的平衡因子为1。

        二:插入的位置在subLR的右子树,我们同样需要更新的还是 parent、subL、subLR三个结点,同样通过观察最后的结果来得出结论。此时subL的平衡因子为-1,subLR的平衡因子为0,parent的平衡因子为0。

        三:插入到subLR位置以后就破坏了平衡,此时旋转的步骤也是一样,但是旋转以后所有的结点的平衡因子都是0。

        这三种情况我们是通过判断插入的数据是在subLR的左子树还是右子树,又或者是插入的数据就是subLR来区分,其实我们可以通过subLR的平衡因子就可以直接区分出三种情况,插入数据以后,当subLR的平衡因子为0时,此时插入的结点的就是subLR;插入结点后subLR的平衡因子是1时,此时插入的位置在subLR的右子树;插入结点后subLR的平衡因子是-1是,此时插入的位置在subLR的左子树。

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

		RotateL(parent->_left);
		RotateR(parent);

		if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_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
			assert(false);
	}

        4、右左双旋

       右左双旋 的旋转方式和左右双旋一样,这里我就直接给出三种情况的结果图。

        当subRL的平衡因子为0时,parent、subR、subRL的平衡因子都是0,;当subRL的平衡为-1时,parent、subRL的平衡因子为0,subR的平衡因子为1;当subRL的平衡因子为1时,subRL、subR的平衡因子为0,parent的平衡因子为-1。

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

		RotateR(parent->_right);
		RotateL(parent);

		if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_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
			assert(false);
	}

        通过parent和cur的平衡因子就能判断出需要使用哪种旋转,完成的插入操作代码如下:

	//插入结点
	bool Insert(const T& data)
	{ 
		if (_root == nullptr)
		{
			_root = new Node(data);
			return true;
		}

		//找插入的位置
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
				assert(false);
		}

		cur = new Node(data);
		if (parent->_data > data)
			parent->_left = cur;
		else
			parent->_right = cur;
		cur->_parent = parent;

		//更新平衡因子
		while (parent)
		{
			if (cur == parent->_left)
				parent->_bf--;
			else
				parent->_bf++;

			if (parent->_bf == 0)
				break;
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//旋转
				if (parent->_bf == -2 && cur->_bf == -1)
					RotateR(parent);
				else if (parent->_bf == 2 && cur->_bf == 1)
					RotateL(parent);
				else if (parent->_bf == -2 && cur->_bf == 1)
					RotateLR(parent);
				else if (parent->_bf == 2 && cur->_bf == -1)
					RotateRL(parent);
				else
					assert(false);
				break;
			}
			else
				assert(false);
		}

		return true;
	}

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

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

相关文章

不同时期的USB接口

Type-A Type-A接口最早于USB1.0标准(1996)推出&#xff0c;拥有四个引脚&#xff1a;VBUS提供5V电源&#xff0c;D-和D用于数据传输&#xff0c;GND接地。 Type-B Type-B接口最早于USB1.0标准(1996)推出&#xff0c;拥有四个引脚&#xff1a;VBUS提供5V电源&#xff0c;D-和D用…

QD1-P7 HTML常用标签:div和span

本节学习&#xff1a;div 和 span 标签。 本节视频 www.bilibili.com/video/BV1n64y1U7oj?p7 ‍ 一、div 标签 用途 ​<div>​ 标签在 HTML 中是一个通用 容器 &#xff0c;用于将 HTML 文档中的内容分组并在文档中划分区域。<div> ​元素本身不具有特定的含…

道路积水检测数据集 1450张 路面积水 带分割 voc yolo

道路积水检测数据集 1450张 路面积水 带分割 voc yolo 分类名: (图片张数&#xff0c; 标注个数) puddle:(1468,1994) 总数:(1468&#xff0c;1994) 总类(nc): 1类 道路积水检测数据集介绍 项目名称 道路积水检测数据集 项目概述 本数据集包含1450张带有标注的图像&#x…

【ubuntu】ubuntu20.04安装cuda12.6与显卡驱动

目录 1.安装cuda12.6 2.安装显卡驱动 1.安装cuda12.6 https://developer.nvidia.com/cuda-toolkit-archive https://developer.nvidia.com/cuda-12-6-0-download-archive?target_osLinux&target_archx86_64&DistributionUbuntu&target_version20.04&target_…

记一次 stm32f407 无法进入 standby 问题

记一次 stm32f407 无法进入 standby 问题 通过查看当前中断信息,发现是 systick 中断pending未处理导致进入standby 模式的 WFI 失败,所以需要在执行 WFI 之前清除 systick 中断pending标志. 查看<Cortex M3与M4权威指南>如下: 可知ICSR寄存器的bit 26表示systick中断是…

RadioGroup RadioButton底部导航栏

参考: https://blog.csdn.net/lu202032/article/details/117632709 activity_home.xml <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:app"http://sch…

八大排序--07归并排序

假设数组 arr[] {5,7,4,2,0,1,6},请通过插入排序的方式&#xff0c;实现从小到大排列&#xff1a; 方法&#xff1a;先拆分&#xff0c;再合并&#xff0c;并在合并过程中结束临时空间进行排序&#xff1b; 拆分&#xff1a;从待排序列中间位置拆开&#xff0c;数据分成左右两…

windows C++-创建图像处理的异步消息(二)

创建图像处理网络 此部分介绍如何创建对给定目录中的每个 JPEG (.jpg) 图像执行图像处理的异步消息块网络。 网络执行以下图像处理操作&#xff1a; 对于 Tom 创作的任何图像&#xff0c;转换为灰度。 对于任何以红色作为主色的图像&#xff0c;移除绿色和蓝色分量&#xff0…

MCU 移值FreeRTOS:【图文+源代码】

1&#xff1a;裸机程序执行 在裸机程序中&#xff0c;对于简单的方式&#xff0c;经常采用查询方式&#xff0c;即一件事完成后&#xff0c;再去完成另一件事&#xff0c;按照顺序执行&#xff0c;这种执行导致当有紧急情况时&#xff0c;可能会得不到处理。对于更复杂的程序&…

pymupdf 解析 PDF

使用大模型处理文档时&#xff0c;需要对二进制格式的文档进转解析提取文字和图片&#xff0c;本文使用 pymupdf 开源库&#xff0c;对 PDF 进行解析提取文字和图片。 安装依赖 首先安装 pymupdf 依赖 pymupdf4llm0.0.17 pymupdf1.24.10 apscheduler3.10.4PDF 转 Markdown …

MyBatis-Plus 之 typeHandler 的使用

一、typeHandler 的使用 1、存储json格式字段 如果字段需要存储为json格式&#xff0c;可以使用JacksonTypeHandler处理器。使用方式非常简单&#xff0c;如下所示&#xff1a; 在domain实体类里面要加上&#xff0c;两个注解 TableName(autoResultMap true) 表示自动…

等级保护等保资料原件合集(word源资料)

第二章 系统定级与安全域 2.1 系统定级 2.1.1 不同等级的安全保护能力 2.1.2 重要信息系统 2.1.3 定级参考 2.2 安全域定义 2.2.1 安全域定义方法 2.2.2 安全域等级描述 第三章 实施方案设计 3.1 三级等保要求 3.2 基本要求的详细技术要求 3.2.1 物理安全 3.2.2 网…

非线性关卡设计

【GDC】如何设计完全非线性的单人关卡_DOOM (bilibili.com) 本文章算是此视频的简单笔记&#xff0c;更详细还请看视频 设计完全非线性关卡强调自由移动和沙盒式玩法&#xff0c;鼓励玩家进行不可预测的移动和空间探索。讲解者分享了设计此类关卡的具体步骤&#xff0c;包括明…

element ui 使用

文章目录 element ui1.组件内部传值使用说明&#xff1a;当我们在app组件中使用movie组件&#xff0c;我们希望movie组件的内容是由app组件来定义&#xff0c;就可以使用prop关键字1.在app组件中导入movie组件并且使用2.在movie中写死数据测试3.使用date测试4.使用props 2.elem…

排序--DS

1. 排序 所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 #稳定性&#xff1a; 假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经过排序&#xff0c;这些记录…

CentOS 7 yum命令报错...

例如 yum install wget 可以看到是报错的 解决方案 对系统本身的 yum 源进行备份 进入源目录 cd /etc/yum.repos.d/备份 sudo cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup下载阿里云 CentOS 7 镜像源配置文件 sudo curl -o /etc/yum…

安装R和RStudio:开始你的数据分析之旅

数据分析是当今世界中一个非常热门的领域&#xff0c;而R语言是进行数据分析的强大工具之一。R是一种编程语言和软件环境&#xff0c;用于统计计算和图形表示。RStudio是一个集成开发环境&#xff08;IDE&#xff09;&#xff0c;它为R语言提供了一个更加友好和高效的工作环境。…

Java | Leetcode Java题解之第464题我能赢吗

题目&#xff1a; 题解&#xff1a; class Solution {Map<Integer, Boolean> memo new HashMap<Integer, Boolean>();public boolean canIWin(int maxChoosableInteger, int desiredTotal) {if ((1 maxChoosableInteger) * (maxChoosableInteger) / 2 < desi…

github创建仓库并本地使用流程,以及问题src refspec xxx does not match any

1.在 GitHub 上创建一个新仓库 登录你的 GitHub 账户。 点击右上角的 “” 按钮&#xff0c;然后选择 “New repository”。 填写仓库名称&#xff08;如 my-repo&#xff09;。 &#xff08;可选&#xff09;添加描述&#xff0c;选择是否公开或私有仓库。 &#xff08;可选&…

山羊检测系统源码分享

山羊检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vision 研究…