【平衡二叉搜索树(AVL)-- 旋转】

news2024/12/28 13:33:05

前言

打怪升级:第60天
在这里插入图片描述

AVLTree,也就是我们所说的:自平衡二叉搜索树,AVL命名由来是两位发明者的名字的首字母,并无其他含义。
AVL树有两个重要的特点:

  1. AVL树是一棵搜索树;
  2. AVL树左右子树的高度差的绝对值不大于1;
  3. AVL树的左右子树也是AVL树。
    高度差可取0,1,-1。

注:我们将左右子树的高度差称为平衡因子,简称为bf(Balance Factor)。

  • 既然AVL树是一棵搜索树它就需要满足搜索树的特征:
  1. 左子树不空,左子树上的值都小于根节点的值;
  2. 右子树不空,右子树上的值都大于根节点的值;
  3. 左右子树也都是二叉搜索树。
  • 既然要保持AVL树左右子树的高度差的绝对值不大于1,我们就需要记录以及修改它,这里我们采用的方法是旋转

下面我们首先从二叉搜索树的插入开始引入AVL树的插入,以及之后的旋转操作,话不多说,大家上车。


1、二叉搜索树的插入

根据二叉搜索树的性质:左子树节点的值都小于根,右子树节点的值都大于根,我们可以在插入的时候进行一下判断即可,
需要注意的是:如果出现相同的值我们不进行插入。

	template<class K>
	struct BSTreeNode 
	{
		BSTreeNode(const K& key)
			:_left(nullptr)
			,_right(nullptr)
			,_key(key)
		{}
	
		struct BSTreeNode* _left;
		struct BSTreeNode* _right;
		K _key;
	};

	bool Insert(const k& key)
	{
		if (_root == nullptr) // 空树 -- 插入的节点作为根
		{
			_root = new Node(key);
		}
		else
		{
			Node* prev = nullptr; // cur的父节点 
			Node* cur = _root;
			while (cur)  // 小放左,大放右,同不加
			{
				if (key < cur->_key)
				{
					prev = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
					prev = cur;
					cur = cur->_right;
				}
				else
				{
					return false;
				}
			}

			if (key < prev->_key)  // 判断插入到prev的左边还是右边
				prev->_left = new Node(key);
			else
				prev->_right = new Node(key);
		}

		return true;
	}

上面的操作就可以让我们实现二叉搜索树的插入,因为AVL树也是一棵二叉搜索树,他也需要符合二叉搜索树的性质,因此有了二叉搜索树的插入我们就可以很方便的写出AVL树的插入过程,
但是AVL树不仅有二叉搜索树的性质还有自己的一些特性:bf的绝对值不大于1,但是插入的过程中我们只考虑了搜索树的性质,因此在插入之后我们需要检查是否符合AVL树的特性,如果符合我们就不做修改,否则就需要进行旋转。


2、AVL树的旋转

bf的计算我们采用右子树高度 减去 左子树高度

我们来理一理什么时候需要进行旋转:

  1. 插入节点后该节点一定是叶子结点没有左右子树,因此bf为0,
    而插入节点后高度收到影响的就是它的所有祖先节点,因此我们需要从该节点开始往上检查它的祖先节点的bf;

  2. 如果插入之后该节点的祖先节点变成了1/-1, 说明该祖先节点原本是0,此时插入之后高度改变了,我们就需要继续往上更新其他祖先节点。

  3. 而如果插入之后该节点的祖先节点变成了0,说明该祖先节点之前是不平衡的(1/-1),插入之后变成了完全平衡,此时整棵树的高度并没有改变,那么我们就不需要往上更新了。

  4. 既然bf的绝对值不可以大于1,那么当插入一个新的节点后它的某个祖先节点的bf变成了±2,就说明出现了问题,我们需要进行旋转,
    在正常情况下当bf变成±2时我们就要进行旋转,因此不会出现bf绝对值大于2的情况。

在这里插入图片描述

因为插入节点之后我们需要从该节点出发往上检查它的祖先节点,此处我们采用三叉链。
插入的操作同二叉搜索树,下面我们来将上面的分析过程通过代码实现出来:

struct AVLTreeNode
{
	AVLTreeNode(const int& val)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
		,_val(val)
	{}

	AVLTreeNode* _left;
	AVLTreeNode* _right;
	AVLTreeNode* _parent; // 指向父亲
	int _bf; // 平衡因子
	int _val; // 数据
	};

 // insert中的调整操作
		parent = cur->_parent;
		while (parent)
		{
			if (cur == parent->_left) --parent->_bf;
			else ++parent->_bf;

			if (parent->_bf == 0) // 说明我们现在使得父亲的左右平衡了,整体h不变,结束调整
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1) // 父亲的h增加,继续向上调整
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2) // 对父亲进行旋转,之后结束调整
			{
				// 判断如何旋转
				if (parent->_bf == 2 && cur->_bf == 1) // 右右高,左单旋
					RotateL(parent);
				else if (parent->_bf == -2 && cur->_bf == -1) // 左左高,右单旋
					RotateR(parent);
				else if (parent->_bf == 2 && cur->_bf == -1) // 从下往上:左右高,先右旋再左旋
					RotateRL(parent);
				else if (parent->_bf == -2 && cur->_bf == 1) // 从下往上:右左高,先左旋再右旋
					RotateLR(parent);

				break;
			}
			else // 前面就出错了
			{
				assert(false);
			}
		}

旋转操作一共分为4种情况,上方是旋转操作的大框架,下面我们来对它们逐个击破。

(1)右单旋(LL)

右单旋:左子树高,将根节点向右旋转降低左子树的高度并且增加右子树的高度。
由下图我们可以看出 – 经过旋转后三个节点的bf都变为了0 – 根节点的变为了叶子结点,根节点的左子树变为了新的根并且右子树的高度+1。

这里是引用

那么通过上图我们是否可以尝试写出第一份代码?

	void RotateR(Node* parent) // 左高,右单旋
	{
		Node* nodeL = parent->_left;
		
		nodeL->_right = parent;
		parent->_parent = nodeL;
		_root = nodeL; // 更新根节点
		nodeL->parent=nullptr;

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

好像这样就结束了,也没有多复杂嘛,甚至,异常的简单?针对上面的情况我们的确解决了,但是我们来看一看下面的情况:

这里是引用
此时需要旋转的是中间的部分节点,既然是中间的部分,我们就需要链接旧根节点的父节点与新根节点。

在这里插入图片描述
此时需要旋转的部分确实是根节点,不过他好像很特殊,新根节点左右子树都有,我们想要将旧根节点作为新根节点的右子树,就需要先保存新根节点的右子树。

上方的就是我们右旋过程过程中会遇到的所以情况,下面我们对右旋的情况进行总结并且写出完整代码。
右旋的步骤:

  1. 旧根节点作为新根节点的右子树,因此我们需要保存新根节点的右子树;
  2. 新根节点的右子树作为旧根节点的左子树;
  3. 旧根节点改变,因此我们需要更改旧根节点的父亲节点,
  4. 通过上面的分析我们发现:最多有4个节点需要发生更改:旧根节点,旧根节点的父亲节点,旧根节点的左孩子(新根节点),新根节点的右孩子 ,而四个节点可以形成三组父子关系,因此我们在右旋时需要修改三组父子关系

下图中的方块表示一颗颗子树,h为树的高度,希望朋友们可以自行尝试画一画h=0、1 、2。。。的情况,可以加深我们对右旋的理解。
在这里插入图片描述

	void RotateR(Node* parent) // 左高,右单旋
	{
		Node* nodeL = parent->_left;  // 旧根节点的左节点 -- 新根节点
		Node* nodeLR = nodeL->_right;  // 左子树的右子树
		Node* nodePP = parent->_parent; // 旧根节点的父节点

		parent->_left = nodeLR;
		if (nodeLR != nullptr) //  左子树的右子树不为空
			nodeLR->_parent = parent;

		nodeL->_right = parent;
		parent->_parent = nodeL;

		nodeL->_parent = nodePP;
		if (nodePP == nullptr) // 根
		{
			_root = nodeL;
		}
		else // 更新父节点的孩子
		{
			if (nodePP->_left == parent)
				nodePP->_left = nodeL;
			else
				nodePP->_right = nodeL;
		}
		// 更新bf
		parent->_bf = nodeL->_bf = 0; 
	}

动图图解:
在这里插入图片描述在这里插入图片描述在这里插入图片描述

(2)左单旋(RR)

左旋的步骤:

  1. 旧根节点作为新根节点的左子树,因此我们需要保存新根节点的左子树;
  2. 新根节点的左子树作为旧根节点的右子树;
  3. 旧根节点改变,因此我们需要更改旧根节点的父亲节点,
  4. 通过上面的分析我们发现:最多有4个节点需要发生更改:旧根节点,旧根节点的父亲节点,旧根节点的右孩子(新根节点),新根节点的左孩子 ,而四个节点可以形成三组父子关系,因此我们在右旋时需要修改三组父子关系

同右单旋基本一样,下方给出统一图形以及代码解析:

这里是引用

	void RotateL(Node* parent) // 右高,左单旋 -- 或者说叫做右右高,左单旋
	{
		Node* nodeR = parent->_right;
		Node* nodeRL = nodeR->_left;
		Node* nodePP = parent->_parent;

		parent->_right = nodeRL;
		if (nodeRL) nodeRL->_parent = parent;

		nodeR->_left = parent;
		parent->_parent = nodeR;

		if (nodePP) // 不是根节点
		{
			if (parent == nodePP->_left)
				nodePP->_left = nodeR;
			else
				nodePP->_right = nodeR;
		}
		else
		{
			_root = nodeR;
		}
		nodeR->_parent = nodePP;

		// 更改bf
		parent->_bf = nodeR->_bf = 0;
	}

(3)右左双旋(LR)

在左单旋和右单旋的情况下,我们遇到的都是:根节点和它的孩子节点都是同一边高,如下图:
左:parent与nodeL都是左子树高
右:parent与nodeR都是右子树高

这里是引用

下边这种情况:
parent右子树高,nodeR左子树高,此时如果单单使用一次左旋或者右旋无法解决我们不平衡问题。
在这里插入图片描述
这种父亲和孩子高度差不在同一边的情况下我们可以将nodeR的方向转换一下,将nodeR右旋之后parent与nodeR的高度差就达到了统一,
此时再对根节点进行一次左旋就可以达到平衡的目的。

在这里插入图片描述

我们实际上会遇到的情况一共有以下三种:

在这里插入图片描述

只是看图的话好像看不出来点什么,那么我们看一看平衡之后 parent 与 nodeR的bf,新的根节点的bf一定为0,而parent与nodeR的bf却是不断变化的,那么为什么会有出现这三种情况?
这与nodeR的左孩子的孩子有关,它是否有孩子,以及有的是左孩子还是右孩子都会出现不一样的结果,
而单纯的左旋与右旋之后都会将parent与nodeR设置为0,因此这里需要我们进行特殊处理,
我们可以nodeR的左孩子的bf来判断parent与nodeR的bf
具体代码如下:

	void RotateRL(Node* parent) // 从下往上:左右高,先右旋再左旋
	{
		// 旋转
		Node* nodeR = parent->_right;
		Node* nodeRL = nodeR->_left;
		int bf = nodeRL->_bf;   // 用来判断
		RotateR(nodeR);       // 复用
		RotateL(parent);

		// 更改bf
		nodeRL->_bf = 0;
		if (bf == 1)
		{
			parent->_bf = -1;
			nodeR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			nodeR->_bf = 1;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			nodeR->_bf = 0;
		}
		else  // 走到这一步说明在插入新节点之前就出问题了
		{
			assert(false);
		}

	}

在这里插入图片描述

(4)左右双旋(RL)

类比右左双旋。
注:小标题中的 RL指的是:nodeL的右子树高,parent的左子树高
左右双旋指的是:先左旋再右旋

	void RotateLR(Node* parent)  // 从下往上:右左高,先左旋再右旋
	{
		
		Node* nodeL = parent->_left;
		Node* nodeLR = nodeL->_right;
		int bf = nodeLR->_bf;

		// 旋转
		RotateL(nodeL);
		RotateR(parent);

		// 更改bf
		nodeLR->_bf = 0;
		if (bf == -1)
		{
			nodeL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			nodeL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == 0)
		{
			nodeL->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

完整插入代码以及打印验证

#pragma once
#include<iostream>
using namespace std;
#include<cassert>

class AVLTree
{
	typedef AVLTreeNode Node;
public:
	AVLTree()
		:_root(nullptr)
	{}

	bool Insert(const int& p)
	{
		// 插入 -- 找好位置后需要更新bf
		if (_root == nullptr)
		{
			_root = new Node(p);
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_val < p)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_val > p)
			{
				parent = cur;
				cur = cur->_left;
			}
			else  // 已经存在
				return false; 
		}	

		cur = new Node(p);
		if (cur->_kv.first < parent->_kv.first)
			parent->_left = cur;
		else
			parent->_right = cur;
		cur->_parent = parent;

		// 开始向上调整bf,判断是否需要旋转 == 2/-2
		while (parent)
		{
			if (cur == parent->_left) --parent->_bf;
			else ++parent->_bf;

			if (parent->_bf == 0) // 说明我们现在使得父亲的左右平衡了,整体h不变,结束调整
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1) // 父亲的h增加,继续向上调整
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2) // 对父亲进行旋转,之后结束调整
			{
				// 判断如何旋转
				if (parent->_bf == 2 && cur->_bf == 1) // 右右高,左单旋
					RotateL(parent);
				else if (parent->_bf == -2 && cur->_bf == -1) // 左左高,右单旋
					RotateR(parent);
				else if (parent->_bf == 2 && cur->_bf == -1) // 从下往上:左右高,先右旋再左旋
					RotateRL(parent);
				else if (parent->_bf == -2 && cur->_bf == 1) // 从下往上:右左高,先左旋再右旋
					RotateLR(parent);

				break;
			}
			else // 前面就出错了
			{
				assert(false);
			}
		}

		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
	}
private:

	void _InOrder(Node* root)
	{
		if (root == nullptr) return;

		_InOrder(root->_left);
		cout << root->_val << ", \t" << root->_bf << endl;
		_InOrder(root->_right);
	}

private:
	Node* _root = nullptr;
};

3、为什么需要AVL树

有的朋友会有疑问:既然我们已经有了搜索二叉树,而且查找效率也十分不错,为什么还要专门来一个平衡二叉搜索树,这样有必要吗,
嗯~答案肯定是有的,而且,十分有,
在存储数据方面我们有了单链表与数组就已经足够了,之所以更加费事地设计二叉树这种结构并不是因为它长得更优美好看,而是想要利用它,通过对它的存储方式进行限制来达到快速查询的效果 – 二叉搜索树,二叉树在设计之初就不是为了插入和删除。
但是,实际情况下二叉搜索树的形态与插入数据的顺序有很大关系,乱序插入更有利于形成“健康的”二叉搜索树,
如果我的插入的数据是一组接近有序序列,那么得到的二叉树就是一棵“歪脖子树”,甚至是单链表:
在这里插入图片描述

这时对数据的查找效率接近O(N),基本上是遍历一整颗树,因此,在插入过程中我们需要对它进行调整,保证它是一棵“健康的”二叉树,
这样才可以保证查询的高效性:在这里插入图片描述


总结

旋转是为了在保持平衡树性质的前提下降低树的高度,右子树高就左旋,左子树高就右旋。
如果你还有一些疑问未得到解答,可以查看完整代码部分的旋转情况判断,这一些判断条件可以给你很好的启发,配合上自己动手画图,我相信你一定掌握它。

文章中的动图来源:AVL Tree测试



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

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

相关文章

第18章 项目风险管理

文章目录 18.1.2 风险的分类 54318.1.3 风险的性质 544项目风险管理6个过程&#xff08;风险管理、识别风险、实施定性风险分析、实施定量风险分析、规划风险应对、控制风险&#xff09;组织和干系人的风险态度影响因素18.3.3 规划风险管理的输出 550风险识别的原则18.4.2 识别…

vim编辑文件

目录 一、vi和vim &#xff08;1&#xff09;介绍 &#xff08;2&#xff09;相同点 &#xff08;3&#xff09;不同点 二、使用vim打开文件 三、使用vim编辑文件 &#xff08;1&#xff09;vim的四个模式 &#xff08;2&#xff09;命令模式下的编辑命令 删除 复制 …

树莓派4:跑通Tensorflow的Sequential模型用于图片分类

重要提示&#xff1a;由于树莓派相对孱弱的性能&#xff0c;直接在其上训练模型可能花&#xff08;lang4&#xff09;费非常长的时间。本文仅作为示例性的可行性参考&#xff0c;请酌情考虑实验平台。 著名的Tensorflow框架也可以运行在树莓派上。理论还没吃透&#xff0c;但使…

【量化交易笔记】5.SMA,EMA 和WMA区别

股票中的SMA&#xff0c;EMA和WMA是常用的技术分析指标。这些指标基于历史股价计算得出&#xff0c;可以帮助投资者了解股票的趋势&#xff0c;为决策提供依据。虽然它们都是平均值算法&#xff0c;但它们之间还是有一些区别的。 SMA 简单移动平均线&#xff08;Simple Moving…

参与辅助服务的用户侧储能优化配置及经济分析(matlab代码)

目录 1 主要内容 目标函数 2 部分程序 3 程序结果 4 程序链接 1 主要内容 该程序方法复现《参与辅助服务的用户侧储能优化配置及经济分析》&#xff0c;首先&#xff0c; 建立了用户侧储能的全生命周期成本和考虑辅助服务的收益模型&#xff1b;其次&#xff0c;在两部…

一文读懂UML用例图

一、概述 用例是描述系统需求的一种手段&#xff0c;即系统应该做什么。用例图由参与者、用例和主题组成。每个用例的主题都代表了一个用例所适用的系统。用户和任何其他可以与主体交互的系统都被表示为行动者。 用例是一种行为规范。用例的实例指的是紧急行为的发生符合相应…

【前端客栈】基于HTML、CSS、JavaScript的羊了个羊静态仿写页面小游戏

&#x1f3dc;哈喽&#xff0c;大家好&#xff0c;我是小浪。前段时间羊了个羊火遍了大江南北&#xff0c;大家是否都通过第二关了呢&#xff1f;哈哈&#xff0c;没关系&#xff0c;既然通不过&#xff0c;那咋们不如自己来做一个这样的羊了个羊的仿写页面&#xff0c;学会了赶…

文本中的关键词提取方法

目录 1. TF-IDF&#xff08;Term Frequency-Inverse Document Frequency&#xff09;算法&#xff1a; 2. TextRank算法&#xff1a; 3. LDA&#xff08;Latent Dirichlet Allocation&#xff09;算法&#xff1a; 4. RAKE&#xff08;Rapid Automatic Keyword Extraction&…

基于SLM调制器,MIT研发高效率全息显示方案

此前&#xff0c;青亭网曾报道过NVIDIA、三星、剑桥大学等对空间光调制器&#xff08;SLM&#xff09;全息方案的探索。空间光调制器可调节光波的空间分布&#xff0c;在电驱动信号控制下&#xff0c;可改变光在空间中传播的振幅、强度、相位、偏振态等特性&#xff0c;从而形成…

MySQL性能优化之(explain)工具

慢SQL的定位 在MySQL当中&#xff0c;我们有时候写的SQL执行效率太慢此时我们需要将其优化。但是SQL可能非常的多&#xff0c;难道我们一条一条的进行查看吗&#xff1f;在MySQL当当中我们可以查看慢查询日志&#xff0c;看看那些SQL这么慢。但是这个默认情况下这个慢查询日志…

sqoop使用

sqoop使用 1. 导入数据2. 从mysql向hive导入数据2.1 导入用户信息表 2.导入订单表2.2 导入订单表2.3 导入商品信息表2.4 导入国家信息表2.5 导入省份信息表2.6 导入城市信息表2.7 创建hive临时表文件 在使用sqoop之前&#xff0c;需要提前启动hadoop, yarn和对应的数据库mysql …

当音乐遇上Python:用Pydub自动分割音频

&#x1f3b5; &#x1f3b5; &#x1f3b5; 当音乐遇上Python&#xff1a;用Pydub自动分割音频 随着短视频应用的普及&#xff0c;越来越多人开始了解并尝试制作自己的短视频作品。而在制作短视频时&#xff0c;背景音乐的选择和使用也是非常重要的一步。很多人喜欢选择一首长…

倒立摆控制器的设计(分别用极点配置,LQR方法,Robust H-无穷方法)

G01倒立摆控制器设计 Author&#xff1a;DargonNote date&#xff1a;2020/12/13课程用书&#xff1a;LMIs in Control Systems Analysis,Design and Applications 1,倒立摆控制系统简介 倒立摆系统是一个复杂的控制系统&#xff0c;具有非线性、强耦合、多变量、不稳定等特…

干货 | 正念,寻求属于你的存在之道

Hello,大家好&#xff01; 这里是壹脑云科研圈&#xff0c;我是喵君姐姐~ 你是否也曾感到内心无法平静&#xff1f;如果是&#xff0c;不妨了解一下正念&#xff0c;它或许能为你带来改变。 正念作为一种古老的修行方式&#xff0c;如今已经在世界范围内广为流传&#xff0c;…

《Netty》从零开始学netty源码(四十九)之PoolArena

目录 PoolArenaallocate()创建newByteBuf()分配具体的内存空间allocate() PoolArena Netty中分配内存是委托给PoolArena来管理的&#xff0c;它主要有两个实现类&#xff1a; 默认情况下使用的DirectArena&#xff0c;它的数据结构如下&#xff1a; 从属性中我们看到PoolA…

人生若只如初见,你不来看看Django吗

前言 本文介绍python三大主流web框架之一的Django框架的基本使用&#xff0c;如何创建django项目&#xff0c;如何运行django项目以及django项目的目录结构&#xff0c;另外django又是如何返回不同的数据和页面&#xff1f; python三大主流web框架 Python有三大主流的web框架…

JS手写实现Promise.all

Promise.all() 方法接收一个 Promise 对象数组作为参数&#xff0c;返回一个新的 Promise 对象。该 Promise 对象在所有的 Promise 对象都成功时才会成功&#xff0c;其中一个 Promise 对象失败时&#xff0c;则该 Promise 对象立即失败。 本篇博客将手写实现 Promise.all() 方…

用于scATAC-seq有监督分类的Cellcano

细胞类型识别是单细胞数据分析的基本步骤。由于高质量参考数据集的可用性&#xff0c;有监督细胞分类方法在scRNA-seq数据中很受欢迎。染色质可及性分析&#xff08;scATAC-seq&#xff09;的最新技术进步为理解表观遗传异质性带来了新的见解。随着scATAC-seq数据集的不断积累&…