AVL树学习笔记

news2024/10/9 19:59:46

目录

1.AVL树的概念

2.AVL树的实现

2.1AVL树的结构

2.2 AVL树的插入

2.2.1 AVL树插入的大致过程

2.2.2 平衡因子的更新

2.2.3 插入节点及更新平衡因子的代码实现:

2.3 旋转

2.3.1 旋转的原则

2.3.2 右单旋

2.3.3 左单旋

2.3.4 左右双旋

2.3.5 右左双旋


1.AVL树的概念

  • AVL树是自平衡二叉搜索树,可以为一棵空树,或者具备下列性质的二叉搜索树:该树的左右子树也为AVL树,并且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡二叉搜索树,通过控制高度差去控制整棵树的平衡。
  • AVL树中我们将引入平衡因子(balance factor   bf )的概念,树中的每个节点都有一个平衡因子,任何节点的平衡因子就可以反应该节点的左右子树的高度差。在此处我们计算平衡因子(bf)使用右子树高度减去左子树高度,也就是说在该节点的左子树插入时(左子树高度加1),该节点的bf就减1,在该节点的右子树插入时(右子树高度加1),该节点的bf就加1。
  • AVL树任何节点都具有平衡因子,因为规定任何节点的左右子树高度差不超过1,因此任何节点的bf就只有-1/0/1三种情况。当我们更新平衡因子的时候,如果不符合该三种情况,说明我们的AVL树出现了问题,此时我们就需要去解决问题,例如旋转操作。
  • AVL树节点整体节点数量和分布与完全二叉树类似,高度可以控制在logN,那么AVL树的增删查改的效率也可以控制在O(logN),相比于二叉搜索树的极端情况还是有一些效率的提升。

例如上面的二叉树,乍一眼看上去,好像是很平衡的,但其实其中 10 这个节点已经出现了平衡因子为2的情况,对于10这颗子树来说,已经不是AVL树,因此整棵树也就不是AVL树,我们要做的是通过旋转操作来降低树的高度来使之平衡。

2.AVL树的实现

2.1AVL树的结构

和我们之前实现二叉搜索树一样,我们首先需要一个节点的struct:

template<class K,class V>
struct AVLTreeNode
{
	pair<K, V> _value;

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

	int _bf;

	AVLTreeNode(const pair<K, V>& value)
		:_value(value)
		, _parent(nullptr)
		, _left(nullptr)
		, _right(nullptr)
		,_bf(0)
	{}

};

这里的成员变量想重要说_parent,因为我们需要更新每个节点的平衡因子,我们就需要考虑,该节点的祖先的平衡因子,会不会因为在该节点下面插入新节点,也受到影响。因此我们使用三叉链,可以帮助我们快速的找到一个节点的父节点。

接下来是AVL树的整体大框架:

template<class K, class V>
class AVLTree
{
public:
	using Node = AVLTreeNode<K,V>;

private:
    //插入代码

    //旋转代码

    //......

private:
	Node* _root = nullptr;
};

2.2 AVL树的插入

2.2.1 AVL树插入的大致过程

1. 插入一个值的时候,按照二叉搜索树的规则进行插入。

2. 新增加节点后,只会影响祖先节点的高度,也就是说可能会影响部分祖先节点的bf,所以更新平衡因子从 新增节点->根节点 路径上的祖先的平衡因子。所以最坏情况就是我们更新到了根节点,最好情况就是插入之后,该节点的高度不变,也就是说平衡因子变为了0,我们便不再向上更新。

3. 更新bf的过程中,如果没有出现-1/0/1外的问题,那么就表明插入之后,该树仍为AVL树,因此直接插入结束。

4. 更新平衡因子过程中如果出现了不平衡,也就是说bf的绝对值大于1,我们此时就需要对不平衡子树进行旋转操作,旋转后本质降低了子树的高度,不会再影响上一层,所以插入结束。

2.2.2 平衡因子的更新

更新规则:

  • 平衡因子 = 右子树高度-左子树高度。
  • 只有子树高度变化时,才会影响当前节点的平衡因子。
  • 插入节点时,会增加高度所以新增节点在parent的右子树,parent的平衡因子++,反之若在左子树,parent的平衡因子-- 。
  • parent所在子树的高度是否变化决定了是否会继续往上更新。

更新停止条件:

  • 更新后parent的bf = 0,变化过程为 -1->0 或者 1->0,说明了更新前parent子树一边高一边低,新增的节点插入在了低的那边,插入后parent所在子树的高度不变,不会影响parent的父节点的bf,更新结束。
  • 更新后parent的bf = 1/-1,变化过程为 0->1 或者 0->-1,说明更新前的parent子树两边一样高,新增节点插入后,导致了parent所在的子树一边高一边低,parent的子树bf符合平衡要求,但是高度增加了1,会影响parent的父节点的bf,因此我们需要继续向上更新。
  • 更新后parent的bf = 2/-2,变化过程为 -1->-2 或者 1->2,说明更新前parent的子树一边高一边低,新增节点插入在了高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要进行旋转处理。旋转的目的有两个:1、将parent子树旋转平衡。2、降低parent子树的高度,恢复到插入节点以前的高度。所以旋转之后也不需要继续往上更新,插入结束。

还是上面那张图,当我们新插入13节点的时候,一直往上更新到10节点,发现10节点的平衡因子已经变为了2,此时我们需要停止更新,将10节点进行旋转处理。

2.2.3 插入节点及更新平衡因子的代码实现:

bool _Insert(const pair<K, V>& value)
{
	if (_root == nullptr)
	{
		_root = new Node(value);
		return true;
	}
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (value.first < cur->_value.first)
		{
			parent = cur;
			cur = cur->_left;
		}

		else if (value.first > cur->_value.first)
		{
			parent = cur;
			cur = cur->_right;
		}

		else
		{
			return false;
		}
	}
	cur = new Node(value);
	if (cur->_value.first < parent->_value.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
	cur->_parent = parent;

    //该行上方代码,本质上为二叉搜索树插入的逻辑,比较插入的值和该节点的值
    //插入的值小向左树走,反之向右树走,相等直接返回即可。
    
	//更新平衡因子
	while (parent)
	{
        //插入的节点在parent的左边,parent的bf--
		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,停止更新。
			break;
		}

		else//bf出现异常,快速找到原因
		{
			assert(false);
		}
	}
	return true;
}

总的来说,插入(目前不包括旋转代码)的大逻辑还是比较简单的,我们按照二叉搜索树的逻辑进行插入,然后更新平衡因子,如果parent的平衡因子为0,则停止更新;为-1/1,继续向上更新,因为我们是三叉链,很容易向上查找;为2/-2,说明parent的左右子树高度不平衡,需要旋转操作,旋转后停止更新。

2.3 旋转

2.3.1 旋转的原则

  1. 保持搜索树的规则。
  2. 让旋转的子树从不平衡变平衡,其次降低子树的高度。

旋转总共分为四种:右单选/左单旋/右左双选/左右双旋

2.3.2 右单旋

这是右单旋的通用图(-2,-1),我们从中可以看到,a,b,c 三棵树的高度均为h,但是实际上,根节点的左子树多了一个节点5,所以对于10来说是左边高右边低,相差高度为1,因此我们在c插入不会影响整棵树的平衡(前提:a,b,c均符合AVL树),同时也不会在c中发生不平衡现象,否则不会旋转10这个节点。所以我们只讨论在a,b中插入。在b插入我们在后面进行讨论,因为牵扯到双旋,此处只讨论右单旋。

首先对于5节点来说,原来的平衡因子为0,因此原来的5左右子树高度均相等,当在a中插入了一个新节点,也就是5的左子树,那么5节点的平衡因子就需要--,变为了-1,我们之前说过,如果是变为了-1/1,就需要继续向上更新,那么直到根节点,变为-2,此时我们就需要进行旋转。

整个结果来说,就是10的左子树高度变高了,因此我们将10定义为parent,5定义为subL,5的右边定义为subLR,然后改变链接关系即可。

先将10的左(subL)变为5的右(subLR),再将5的右变为10,注意1. 我们这里是三叉链,因此,我们需要将,parent,subL,subLR的父节点(_parent)改变连接方向。如果parent是根节点,我们只需要将subL变为根节点即可,如果parent是一个节点的子树,那么我们需要将subL和parent之前的_parent进行链接。2. 如果a,b,c均为空树,也就是下面的情况:

此时b就是我们定义的subLR为nullptr,对于parent 和 subL,肯定是不为空的,因此我们在链接父子关系时,需要判断一下subLR是否为空。

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


	parent->_left = subLR;
	subL->_right = parent;

	Node* pParent = parent->_parent;//若,parent父节点存在,则提前记录

    //判断subLR是否为空
	if (subLR)
		subLR->_parent = parent;

	parent->_parent = subL;

	if (parent == _root)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		if (pParent->_left == parent)
		{
			pParent->_left = subL;
		}
		else
		{
			pParent->_right = subL;
		}
		subL->_parent = pParent;
	}

    //最终parent和subL的平衡因子均为0
	parent->_bf = 0;
	subL->_bf = 0;
}

以上就是右单旋的逻辑。

2.3.3 左单旋

左单旋的通用图(2,1)如下:

此处的逻辑和右单旋类似,只不过我们定义parent,subR,subRL,将subRL变为parent的左子树,再将parent变为subR的左子树,注意链接关系,以及parent是否为根节点即可,并且要判断subRL是否为空(因为a,b,c可能为空树),最终将subR和parent的平衡因子变为0。

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

	parent->_right = subR->_left;
	subR->_left = parent;

	Node* pParent = parent->_parent;//提前记录

    //判断subRL是否为空
	if (subRL)
		subRL->_parent = parent;

	parent->_parent = subR;
	if (parent == _root)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if (pParent->_left == parent)
			pParent->_left = subR;
		else
			pParent->_right = subR;

		subR->_parent = pParent;
	}

    //将平衡因子变为0
	parent->_bf = 0;
	subR->_bf = 0;
}

2.3.4 左右双旋

我们先来看看下面的两幅图:

这就是我们之前说的,在b插入,最终的结果都是导致10这个节点导致不平衡,如果我们仅仅进行一个右单旋,我们可以看到,旋转之后的树仍然不是平衡的,上面写的平衡因子为0,是因为我们右单旋的代码,本来就使parent和subL的平衡因子变为了0,因此这个时候我们就需要进行双旋

我们来看左右双旋的通用图(-2,1):

我们在b插入,最终都导致了10的平衡因子变为了-2,并且我们需要按上图进行旋转,先对5进行左单旋,再对10进行右单旋,我们只看最终的结果图也就是:

左边是新节点放在了subLR的左边后双旋的结果,右边则放在了subLR的右边双旋后的结果。因此我们提前记录subLR的平衡因子,若为-1,说明最终双旋的结果是左边的结果,若为1,说明最终双旋的结果是右边的结果,并且我们可以发现不同结果所对应的平衡因子只有subL和parent不同。

如果a,b,c均为空树:

那么双旋后,三者的平衡因子均为0即可,左右双旋代码如下:

void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	int bf = subLR->_bf;//提前记录平衡因子,决定最终结果
	RotateL(subL);//先左旋
	RotateR(parent);//再右旋

	if (bf == 0)//a,b,c均为空树情况
	{
		subLR ->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1)//新节点在subLR的右边情况
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == -1)//新节点在subLR的左边情况
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else//如果bf出现意外,快速帮我们找到原因
	{
		assert(false);
	}
}

2.3.5 右左双旋

右左双旋和左右双旋的逻辑类似,这里我们直接来看通用图(2,-1):

这里直接看代码,因为逻辑类似:

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	int bf = subRL->_bf;//提前记录subRL的bf

	RotateR(subR);//先右旋
	RotateL(parent);//再左旋

	if (bf == 0)//a,b,c均为空树
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1)//新节点在subRL的右边
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = -1;
	}

	else if (bf == -1)//新节点在subRL的左边
	{
		subR->_bf = 1;
		subRL->_bf = 0;
		parent->_bf = 0;
	}

	else//若出现bf异常,则快速帮我们找到问题
	{
		assert(false);
	}
}

最后我们来补充插入剩下的旋转部分代码:

//插入中部分代码
//...

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);
	}

	break;
}

//...

以上就是AVL树的学习笔记,供大家参考,如果有任何出错的地方,欢迎大家批评指正!!!

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

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

相关文章

18、电科院FTU检测标准学习笔记-高低温性能试验

作者简介&#xff1a; 本人从事电力系统多年&#xff0c;岗位包含研发&#xff0c;测试&#xff0c;工程等&#xff0c;具有丰富的经验 在配电自动化验收测试以及电科院测试中&#xff0c;本人全程参与&#xff0c;积累了不少现场的经验 ———————————————————…

第二百七十二节 JPA教程 - JPA查询Is Empty示例

JPA教程 - JPA查询Is Empty示例 IS EMPTY运算符是IS NULL的逻辑等价物&#xff0c;但是对于集合。 查询可以使用IS EMPTY运算符或IS NOT EMPTY来检查集合关联路径是否解析为空集合或至少有一个值。 我们可以使用EMPTY来检查属性是否为空。 以下JPQL显示如何使用EMPTY获取员工…

解压缩软件哪个好?不同场景下的最佳选择

解压缩软件在日常工作与生活中发挥着至关重要的作用&#xff0c;从简单的文件解压到处理大型项目&#xff0c;选择一款适合自己的解压缩软件能够大幅提高工作效率。 面对众多解压缩工具&#xff0c;如WinRAR、7-Zip、解压专家、PeaZip等&#xff0c;如何根据不同的使用场景选择…

基于京东:HotKey实现自动缓存热点Key!!!

一.引言 某些热点数据&#xff0c;我们提前如果能够预判到的话&#xff0c;可以提前人工给数据加缓存&#xff0c;也就是缓存预热&#xff0c;将其缓存在本地或者Redis中&#xff0c;提高访问性能同时&#xff0c;减低数据库压力&#xff0c;也减轻后端服务的压力。但是&#…

imageJ 将多图中的同一条划线数据用曲线展示

1、导入图片&#xff0c;将所需的图片放进同一个文件夹&#xff0c;按顺序命名 2、划线 3、导出数据及画曲线 1&#xff09;得到单图的曲线上的图像数据&#xff1a;选中图片 -----> ctrl k 2&#xff09;将多张图像的同一条曲线位置的图像数据在同一数轴上画出曲线 步骤…

SpringBoot中applicaiton.properties转换成application.yml 插件

点击idea中的File----->点击setting----->点击Plugins-----> 输入 Convert Yaml and Properties File ----->然后Install下载 -----> 点击OK应用 最后选择Application.properties----->右击Convert YAML and properties File----->即转换成功了

VUE项目与原生Javascript Js功能模块的整合-政府项目涉及Ukey使用和开发

一、VUE项目与原生Javascript Js功能模块的整合 因为产品要整体做三级等保验证&#xff0c;而等保需要做密码安全评价&#xff08;密评&#xff09;&#xff0c;最终的方案就是需要使用 Ukey 来登录管理后台&#xff0c;而这最终涉及在我们的VUE项目中去调用第三方 UKEY 厂商提…

浙大数据结构:08-图8 How Long Does It Take

这道题算是较为简单的拓扑排序题&#xff0c;难度不大 机翻 1、条件准备 n,m为n个结点&#xff0c;m条边。 tim数组存到该结点完成的最早时间&#xff0c;会一点点更新 graph存有向边的时间 indegree数组存每个结点的入度 #include <iostream> #include <vector&g…

扫描电镜是用来测什么的?

扫描电镜是一种用于对样品进行微观尺度形貌观测和分析的仪器。它能够提供高分辨率的图像&#xff0c;帮助科学家和工程师了解样品的微观结构和特性。 一、扫描电镜的一般测量功能 微观形貌观测 扫描电镜可以清晰地观察到样品表面的微观形貌&#xff0c;如颗粒的形状、大小、…

【Oracle APEX开发小技巧9】通过页面设置文本大写避免upper()函数转换占用额外资源

进行规范改造登录函数和存储过程时&#xff0c;发现有些应用的登录函数/存储过程中有upper()函数的使用&#xff0c;因为登录时输入工号&#xff0c;默认无更改&#xff0c;为了与数据库中存储的数据对应&#xff0c;通过upper()将其文本中字母设置为大写。 若要解决这个问题&…

C++初阶---C++入门(下)

目录 一、内联函数 1.内联函数的定义与底层机制 0x01.内联函数的定义 0x02.内联函数的底层机制 2.内联函数的优缺点 优点&#xff1a; 缺点&#xff1a; 3.内联函数的使用建议 4.内联函数的注意事项 二、auto关键字&#xff08;C11&#xff09; 1.代码示例 2.auto使…

flask项目框架搭建

目录结构 blueprints python包&#xff0c;蓝图文件&#xff0c;相当于路由组的概念,方便模块化开发 例如auth.py文件 from flask import Blueprint, render_templatebp Blueprint("auth", __name__, url_prefix"/auth")bp.route("/login") d…

李沐 X 动手学深度学习 深度学习介绍 学习笔记

x轴是不同的模式&#xff1a;符号学---概率模型---机器学习y轴是我们想做的东西&#xff08;问题领域&#xff09;&#xff1a;感知&#xff08;了解这是什么东西&#xff0c;能看见这个物体&#xff09;---&#xff08;做&#xff09;推理&#xff08;基于我看到的东西想象未来…

揭秘猫咪掉毛的真实原因有哪些?掉毛飞毛宠物空气净化器来救援!

作为三猫家庭&#xff0c;日常家里的毛发、异味一直是困扰我很久的难题。最近窗外的世界柳絮满天飘&#xff0c;家里猫毛满飞&#xff0c;养猫家庭应该都不陌生吧&#xff0c;鼻子嘴巴甚至是眼睛里总感觉有猫毛。不管猫砂换的多勤快&#xff0c;也总能闻到阵阵臭味&#xff01;…

【git】git add时warning:LF will replaced by CRLF

git add时warning&#xff1a;LF will replaced by CRLF 一&#xff0c;问题现象二&#xff0c;问题原因&解决方法 一&#xff0c;问题现象 二&#xff0c;问题原因&解决方法 这个警告的原因是 Git 在进行文件添加操作时&#xff0c;发现行尾结束符不一致。 在不同的…

Klick‘r3.0.4 |智能自动点击,高效省力

Klick’r 是一款专为 Android 设计的开源自动点击工具&#xff0c;能识别屏幕上的图像并进行相应操作。支持游戏中的自动点击、应用测试及日常任务自动化。 大小&#xff1a;27M 百度网盘&#xff1a;https://pan.baidu.com/s/1881Zfevph6_2Zhdc-H_R4A?pwdolxt 夸克网盘&…

定点数和浮点数的详细介绍(一)定义、范围、位宽

1.定点数 1.1定点数描述 定点数包括定点小数(纯小数)、定点整数(纯整数)、整数和小数位数固定的实数。 1.2定点小数: 小数点默认在符号位后面,首位为符号位,其他为数值位(在用二进制代表小数时,例如0xFF,就表示0.5+0.25+0.125+0.0625+0.03125........) 例如,用…

【p2p、分布式,区块链笔记 UPNP】: Libupnp的线程池简述

线程池在网络编程中是一个关键的组成部分&#xff0c;尤其是处理高并发请求时&#xff0c;线程池可以显著提高系统的性能和资源利用效率。它的关键组成部分包括以下几个要素&#xff1a;任务队列&#xff1a;一个&#xff08;或多个&#xff09;用于存放待执行任务的队列。任务…

简单花20分钟学会top 命令手册 (linux上的任务管理器)

1. 介绍 top 是一个常用的 Linux 命令行工具&#xff0c;用于实时监视系统资源和进程的运行情况。用户可以通过 top 命令查看系统的 CPU 使用率、内存占用情况、进程列表等重要信息&#xff0c;帮助快速了解系统运行状态并进行性能监控。该工具可以认为相当于windows上的任务管…

什么是MAC地址?有必要隐藏MAC地址吗?

你是否曾经停下来思考&#xff0c;每当你上网时&#xff0c;你的数字足迹可能会泄露你的个人信息&#xff1f;可能你会问&#xff0c;MAC地址是什么&#xff1f;简单来说&#xff0c;每台联网的电脑MAC地址都是独一无二的&#xff0c;就像是你设备在网络世界中的身份证。它能帮…