平衡BST:AVL树的实现与机制

news2024/10/4 21:10:34

目录

AVL树的简介

AVL节点的构建

AVL树体的构建

具体片段解析

旋转算法

AVL树的验证


AVL树的简介

AVL树是一种自平衡的二叉搜索树,它在19世纪60年代由Adelson-Velsky和Landis首次提出。在AVL树中,任何节点的两个子树的高度最大差别为1,这种平衡确保了树的操作(如插入、删除和查找)的时间复杂度为O(log n)。下面是AVL树在C++中的基本概念:

基本概念:

  • 节点:AVL树的每个节点包含关键值、指向左右子树的指针以及一个表示该节点高度或平衡因子的值。
  • 平衡因子:节点左右子树的高度差。在AVL树中,每个节点的平衡因子只能是-1、0或1。
  • 旋转:为了维护树的平衡,AVL树在插入或删除节点后可能会进行旋转。旋转分为四种类型:右旋、左旋、左右旋和右左旋。
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查
找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii
和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
它的左右子树都是AVL树
左右子树高度之差(简称平衡因子)的绝对值不超过1
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在LogN,时间复杂度也是LogN
因此AVL树的构建是十分繁琐的,但是效率及其高效。

AVL节点的构建

我们构建一个K/V结构的AVL树,用来存储键值对。

由于牵扯到树形的连接,因此我们采用三叉链的形式,内部存储三个指针

同时_bf(binary factor)平衡因子用来检测树书否符规范


template<typename K, typename V>
struct AVLTreeNode {
	//三叉连锁树的节点结构
	AVLTreeNode* _left;		//C++将struct之后的部分升级成了类名,可以直接用来创建对象
	AVLTreeNode* _right;
	AVLTreeNode* _parent;

	pair<K, V> _kv;
	int _bf;		//平衡因子,左子树的高度减去右子树的高度  balance factor

	AVLTreeNode(const pair<K, V>& kv)
		: _left(nullptr),
		_right(nullptr),
		_parent(nullptr),
		_kv(kv),	//调用默认构造函数,初始化_kv
		_bf(0)
	{}
};

AVL树体的构建

成员变量只需要一个树根

Node* _root;

AVL树体重点是插入的实现

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么
AVL树的插入过程可以分为两步:
1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子
插入具体分为三部分:    //1.插入 2.连接 3.调整
	bool insert(const pair<K, V>& kv)
	{

		if (_root == nullptr) {
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}

		//cur为空,说明找到了插入位置
		
		cur = new Node(kv);
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;	//连接
			cur->_parent = parent;
		}
		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->_right->_bf == -2)	//调整(插入之前不可能为2或者-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);

				// 1、旋转让这颗子树平衡了
				// 2、旋转降低了这颗子树的高度,恢复到跟插入前一样的高度,所以对上一层没有影响,不用继续更新
				break;
			}

			else
				assert(false);
		}

	}

具体片段解析

1.空树直接插入


		if (_root == nullptr) {
			_root = new Node(kv);
			return true;
		}

2.寻找合适的位置插入(找到空位置)        

while (cur)
{
	if (kv.first < cur->_kv.first)
	{
		parent = cur;
		cur = cur->_left;
	}
	else if (kv.first > cur->_kv.first)
	{
		parent = cur;
		cur = cur->_right;
	}
	else
	{
		return false;
	}
}

3.插入后进行连接

if (kv.first < parent->_kv.first)
{
	parent->_left = cur;	//连接
	cur->_parent = parent;
}
else
{
	parent->_right = cur;
	cur->_parent = parent;
}

4.进行平衡因子的调整(重难点)

4.1根据插入的位置,插入左树,_bf--,否则++

	if (cur == parent->_left)
		parent->_bf--;
	else
		parent->_bf++;

4.2平衡因子发生变化之后,进行调整

若插入之后,abs(_bf) == 1,那说明该树处于临界状态,此时需要向上检查

若abs(_bf) == 2,说明插入之后,树已经失衡,需要进行旋转调整

同时,旋转之后,不仅完成了树的平衡,还使得高度--,树变成了平衡状态

此时abs(parent->_bf) == 2,abs(cur->_bf) == 1,所以四种情况

if (parent->_bf == 0)
	break;		//平衡
else if (parent->_bf == 1 || parent->_bf == -1)		//需要向上检查
{
	cur = parent;
	parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_right->_bf == -2)	//调整(插入之前不可能为2或者-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);

	// 1、旋转让这颗子树平衡了
	// 2、旋转降低了这颗子树的高度,恢复到跟插入前一样的高度,所以对上一层没有影响,不用继续更新
	break;
}

else
	assert(false);

旋转算法

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,
使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
1. 新节点插入较高左子树的左侧---左左:右单旋
核心就是对SubL 和SubRL( 注意是否为空)这两个节点的处理。
同时需要注意parent是不是根节点
上图在插入前, AVL 树是平衡的,新节点插入到 30 的左子树 ( 注意:此处不是左孩子 ) 中, 30
子树增加
了一层,导致以 60 为根的二叉树不平衡,要让 60 平衡,只能将 60 左子树的高度减少一层,右子
树增加一层,
即将左子树往上提,这样 60 转下来,因为 60 30 大,只能将其放在 30 的右子树,而如果 30
右子树,右子树根的值一定大于 30 ,小于 60 ,只能将其放在 60 的左子树,旋转完成后,更新节点
的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:
1. 30 节点的右孩子可能存在,也可能不存在
2. 60 可能是根节点,也可能是子树
   
如果是根节点,旋转完成后,要更新根节点
   
如果是子树,可能是某个节点的左子树,也可能是右子树
    
//1旋转 2调整bf
 
	void RotateR(Node* parent)
	{
		Node* SubL = parent->_left;			//最关键的两个节点
		Node* SubLR = SubL->_right;

		//连接
		parent->_left = SubLR;
		SubL->_right = parent;

		//处理剩余关系线
		parent->_parent = SubL;

		if (SubLR)    //如果subLR为空,那么parent->right一定为空
			SubLR->_parent = parent;

		Node* parentParent = parent->_parent;

		if (_root == parent)	    //有可能parent是根节点,也有可能不是根节点
		{
			_root = SubL;
			subL->_parent = nullptr;
		}
		else  //不是根节点
		{
			if (parent == parentParent->_left)	 //判断是左子树还是右子树
			{
				parentParent->_left = SubL;
				subL->_parent = parentParent;
			}
			else
			{
				parentParent->_right = SubL;
				subL->_parent = parentParent;
			}
		}

		subL->_bf = 0;	
		parent->_bf = 0;  //如果subLR为空,那么parent->right一定为空,最终的_bf一定都是空
	}
2. 新节点插入较高右子树的右侧---右右:左单旋
	void RotateL(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;

		//连接
		parent->_right = SubRL;
		SubR->_left = parent;

		//处理剩余关系线

		parent->_parent = SubR;

		if (SubRL)		//如果subRL为空,那么parent->left一定为空
			SubRL->_parent = parent;

		Node* parentParent = parent->_parent;

		if (_root == parent)		//有可能parent是根节点,也有可能不是根节点
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == parentParent->_left)	//判断是左子树还是右子树
			{
				parentParent->_left = SubR;
				subR->_parent = parentParent;
			}
			else
			{
				parentParent->_right = SubR;
				subR->_parent = parentParent;
			}
		}

		subR->_bf = 0;
		parent->_bf = 0;	//如果subRL为空,那么parent->left一定为空,最终的_bf一定都是空
	
	}
3. 新节点插入较高左子树的右侧---左右:先左单旋再右单旋
将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再
考虑平衡因子的更新。
需要考虑的是,插入之后_bf的调整。
同时每次插入都需要考虑新插入的节点是不是空节点
	void RotateLR(Node* parent)
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;  //可能为空
		int bf = SubLR->_bf;         //可能为0(如果为0,说明SubLR为空)

		RotateL(SubL);
		RotateR(parent);

		if (bf == 0)
		{
			//说明SubLR就是新增
			parent->_bf = subL->_bf = subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			//说明SubLR是新增的右子树
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			//说明SubLR是新增的左子树
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
4. 新节点插入较高右子树的左侧---右左:先右单旋再左单旋
	void RotateRL(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;  //可能为空
		int bf = SubRL->_bf;         //可能为0(如果为0,说明SubRL为空)

		RotateR(SubR);
		RotateL(parent);

		if (bf == 0)
		{
			//说明SubRL就是新增
			parent->_bf = subR->_bf = subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			//说明SubRL是新增的右子树
			parent->_bf = 0;
			subR->_bf = -1;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			//说明SubRL是新增的左子树
			parent->_bf = 1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}

	}

        

总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
当pSubR的平衡因子为1时,执行左单旋
当pSubR的平衡因子为-1时,执行右左双旋
2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
当pSubL的平衡因子为-1是,执行右单旋
当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
1. 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确
采用了双重保险:1.检测右-左是否为_bf  2.检测abs(_bf)是否为<2
在高度求解时,采用了递归算法。求左子树高度(求子树高度不能 + 1) ,再求右子树高度。总高度 = max(左子树,右子树) + 1        (在algorithm算法库中)
int _Height(Node* root)
{
	if (root == nullptr)
		return 0;

	int leftHeight = _Height(root->_left);	//求的是子树的高度,不包括自身,不能加1
	int rightHeight = _Height(root->_right);

	return max(leftHeight, rightHeight) + 1;
}

bool _IsBalanced(Node* node)	//进行遍历
{
	if (node == nullptr)
		return true;

	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);

	if (rightHeight - leftHeight != root->_bf)
	{
		cout << root->_kv.first << "平衡因子异常" << endl;
		return false;
	}

	return abs(rightHeight - leftHeight) < 2
		&& _IsBalance(root->_left)
		&& _IsBalance(root->_right);
}

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

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

相关文章

python-FILIP/字符串p形编码/数字三角形

一&#xff1a;FILIP 题目描述 给你两个十进制正整数 a,b​&#xff0c;输出将这两个数翻转后的较大数。 「翻转」在本题中的定义详见「说明 / 提示」部分。输入 第一行&#xff0c;两个十进制正整数 a,b。输出 第一行&#xff0c;a 和 b 翻转后的较大数。样例输入1 734 893 样…

《凡人修仙传》TXT精校全本|知轩藏书校对版!

看了动漫版&#xff0c;准备重温下原著&#xff0c;有好几年没看了。 最近找到了知轩藏书的校对版&#xff0c;堪称精校&#xff0c;nice&#xff01; TXT&#xff0c;14.5MB&#xff1a; https://pan.quark.cn/s/c6446be393fa

二叉树进阶学习——从中序和后续遍历序列构建二叉树

1.题目解析 题目来源&#xff1a;106.从中序和后序遍历序列构造二叉树 测试用例 2.算法原理 后序遍历&#xff1a;按照左子树->右子树->根节点的顺序遍历二叉树&#xff0c;也就是说最末尾的节点是最上面的根节点 中序遍历&#xff1a;按照左子树->根节点->右子树…

gm/ID设计方法学习笔记(一)

前言&#xff1a;为什么需要gm/id &#xff08;一&#xff09;主流设计方法往往侧重于强反型区&#xff08;过驱>0.2V&#xff09;&#xff0c;低功耗设计则侧重于弱反型区&#xff08;<0&#xff09;&#xff0c;但现在缺乏对中反型区的简单和准确的手算模型。 1.对于…

C++系列-二叉搜索树

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 二叉搜索树 二叉搜索树又称二叉排序树&#xff0c;它或者是一颗空树&#xff0c;或者是具有以下性质的树 若它的左子树不为空&#xff0c;则左子树上的所有节点的值都小于根节点…

大数据实时数仓Hologres(四):基于Flink+Hologres搭建实时数仓

文章目录 基于FlinkHologres搭建实时数仓 一、使用示例 二、方案架构 1、架构优势 2、Hologres核心优势 三、实践场景 四、项目准备 1、创建阿里云账号AccessKey 2、准备MySQL数据源 五、构建实时数仓​编辑 1、管理元数据 2、构建ODS层 2.1、创建CDAS同步作业OD…

鸿蒙网络管理模块03——多播DNS管理

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 1、概述 多播DNS也简称MDNS(Multicast DNS)&#xff0c;他主要提供局域网内的本地服…

NVIDIA Ampere 架构

全球超强弹性数据中心的核心。 文章目录 前言一、突破性创新1. 第三代 Tensor 核心2. 多实例 GPU (MIG)3. 第三代 NVLink4. 结构化稀疏5. 第二代 RT 核心6. 更聪明、快速的内存二、为规模化部署而优化1. 为各种服务器优化性能2. 统一计算和网络加速3. 密度优化的设计4. 安全部署…

leetcode练习 路径总和II

给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum 22 输出&a…

ElasticSearch备考 -- 查询模版

一、题目 ### 基础版 Create a search template for the above query, so that the template (i) is named "with_response_and_tag", (ii) has a parameter "with_min_response" to represent the lower bound of the response field, (iii) has a parame…

二叉树进阶学习——从前序和中序遍历序列构造二叉树

1.题目解析 题目来源&#xff1a;105.从前序与中序遍历序列构造二叉树——力扣 测试用例 2.算法原理 首先要了解一个概念 前序遍历&#xff1a;按照 根节点->左子树->右子树的顺序遍历二叉树 中序遍历&#xff1a;按照 左子树->根节点->右子树的顺序遍历二叉树 题目…

10款好用的开源 HarmonyOS 工具库

大家好&#xff0c;我是 V 哥&#xff0c;今天给大家分享10款好用的 HarmonyOS的工具库&#xff0c;在开发鸿蒙应用时可以用下&#xff0c;好用的工具可以简化代码&#xff0c;让你写出优雅的应用来。废话不多说&#xff0c;马上开整。 1. efTool efTool是一个功能丰富且易用…

java入门基础(一篇搞懂)

​ 如果您觉得这篇文章对您有帮助的话 欢迎您分享给更多人哦 感谢大家的点赞收藏评论&#xff0c;感谢您的支持&#xff01;&#xff01;&#xff01; 首先给大家推荐比特博哥&#xff0c;java入门安装的JDk和IDEA社区版的安装视频 JDK安装与环境变量的配置 IDEA社区的安装与使…

多线程-初阶(1)

本节⽬标 • 认识多线程 • 掌握多线程程序的编写 • 掌握多线程的状态 • 掌握什么是线程不安全及解决思路 • 掌握 synchronized、volatile 关键字 1. 认识线程&#xff08;Thread&#xff09; 1.1 概念 1) 线程是什么 ⼀个线程就是⼀个 "执⾏流". 每个线…

数据在内存中的存储【上】

一.整型在内存中的存储 在讲解操作符的时候&#xff0c;我们就讲过了下面的内容&#xff1a; 整数的2进制表示方法有三种&#xff0c;即 原码、反码和补码 有符号的整数&#xff0c;三种表示方法均有符号位和数值位两部分&#xff0c;符号位都是用0表示"正"&#xff…

Java之队列

1. 概念 队列&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性 特点&#xff1a; 队列具有先进先出FIFO(First In First Out) 入队列&#xff1a;进行插入操作的一端称为队尾&#xff08;Tail/Rear&#xff09; 出队列&#xff1a;进…

Pikachu-Sql-Inject - 基于时间的盲注

基于时间的盲注&#xff1a; 就是前端的基于time 的盲注&#xff0c;什么错误信息都看不到&#xff0c;但是还可以通过特定的输入&#xff0c;判断后台的执行时间&#xff0c;从而确定注入。 mysql 里函数sleep() 是延时的意思&#xff0c;sleep(10)就是数据库延时10 秒返回内…

【C++】异常处理

目录 一、C语言中传统的异常处理方式&#xff1a; 二、C中的异常处理方式&#xff1a; 三、异常的使用 1、关于抛出与捕获&#xff1a; 2、关于异常的抛出和匹配&#xff1a; 3、异常的重新抛出&#xff1a; 4、异常安全&#xff1a; 5、异常规范&#xff1a; 四、异常…

idea 同一个项目不同模块如何设置不同的jdk版本

在IntelliJ IDEA中&#xff0c;可以为同一个项目中的不同模块设置不同的JDK版本。这样做可以让你在同一个项目中同时使用多个Java版本&#xff0c;这对于需要兼容多个Java版本的开发非常有用。以下是设置步骤&#xff1a; 打开项目设置&#xff1a; 在IDEA中&#xff0c;打开你…

Git 下载及安装超详教程(2024)

操作环境&#xff1a;Win 10、全程联网 一、什么是Git&#xff1f; Git 是一个开源的分布式版本控制系统&#xff0c;由 Linus Torvalds 创立&#xff0c;用于有效、高速地处理从小到大的项目版本管理。Git 是目前世界上最流行的版本控制系统&#xff0c;被广泛用于软件开发中…