C++ - AVL树

news2024/11/26 7:52:25

之前的文章中我们学习过二叉搜索树,学习完该部分之后,在进行OJ的练习和思考中会发现如果一颗搜索树由于初始结点选择的不好这棵树就会变成成一颗歪脖子树,这样搜索的效率反而会变的不是很理想。那么在今天的文章中我们就要来介绍一种基于搜索树的树 -- AVL树。

AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查
找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii
和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

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

下图就是一个典型的AVL树,在这里我们讲述的AVL树使用的是平衡因子来进行的平衡。其中每个结点的平衡因子表示其左右子树的高度之差,以8结点为例子就是右子树的高度减去左子树的高度的差即1。

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在

O(log_2 n),搜索时间复杂度O(log_2 n)。

AVL树节点的定义

在定义AVL树的结点的时候,与搜索树不同的地方是,在这里我们定义了一个父亲节点,有了这个节点之后再插入的时候,会带来一些便捷,但也会有一些复杂。此外,还定义了平衡因子。

template<class K, class V>
class AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	int _bf; // balance factor

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

AVL树的插入

因为AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树的插入与搜索树是类似的。AVL树的插入过程可以分为两步:

  • 按照二叉搜索树的方式插入新节点
  • 调整结点的平衡因子
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 (cur->_kv.first < kv.first) {
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first) {
				parent = cur;
				cur = cur->_left;
			}
			else {
				return false;
			}
		}

		cur = new Node(kv);
		if (parent->_kv.first > kv.first) {
			parent->_left = cur;
		}
		else {
			parent->_right = cur;
		}
		cur->_parent = parent;
		// 更新平衡因子

		return true;
	}

在插入中需要注意的就是,由于我们新添增了父亲结点,因此需要在插入时将父亲结点链接。

由于插入之后的可能性会有很多,现在就让我们来分析一下,下面就是一些插入时候可能会发生的情况:

从图中可以看出这样的信息:

  • 如果更新完之后,平衡没有出现问题(|bf| <= 1),平衡结构没有受到影响,不需要处理。
  • 如果更新完之后,平衡出现问题(|bf| > 1),平衡结构受到影响,需要处理(旋转)
  • 插入新增节点,会影响祖先的平因子(全部或者部分)
  • 祖先结点的变化方式为:cur == parent->right parent->bf++; cur == parent->left parent->bf--;

然后我们对parent的平衡因子进行分析:

  1. parent == 1 || parent == -1,parent的子树高度发生了变化,需要继续往上更新,因为1和-1是从0转换过来的,插入之前是parent->bf == 0,说明左右两边高度相等,现在有一边高1,说明parent有一边高一边低,高度发生了变化;
  2. parent == 0,parent的子树高度不变,不用继续往上更新,这一插入结束。因为0是从1和-1转换过来的,插入之前是parent->bf == 1 || parent->bf == -1,说明插入之前一边高一边低,插入节点填上矮的那边,高度不变。
  3. parent == 1 || parent == -1,parent所在子树不平衡,需要处理这棵子树(旋转处理)  

下面我们就来详细的介绍一下AVL树的旋转:

AVL树的旋转

根据前文中AVL树的插入所示,祖先结点的最终变化一定为2或者-2,那么它一定有一个孩子结点的平衡因子是1和-1,有了高度的变化才会引起祖先结点的变化。

新节点插入较高左子树的左侧 -- 右单旋

右单旋情况的介绍:

这个是一个左单旋的示意图。a/b/c分别代表着一棵高度为h的子树。新插入的结点在a子树中,下面我们先进行一些假设:

假设abc为空,在3号结点的左子树位置新增一个结点,然后对平衡因子进行修正可得;

假设abc为一棵高度为1的子树,在a树随意一个孩子结点上新增一个节点,然后对平衡因子进行修正可得;

假设abc为一颗高度为2的子树,由于高度为2的子树有三种不同情况,但是我们这里要保证a子树一定是完全二叉树,其余的bc两棵子树随意什么形状都可以。这样是因为如果a子树不是一颗满树,那么在插入的时候就有可能不会对祖先结点的平衡因子造成影响,有可能会正好插入在空缺的那一个位置上。

......随着abc树高度的变化,还有着很多的情况,但是这些情况都有着一个特点就是,在新增结点之后祖先的平衡因子一定会变为-2,其左孩子的结点一定会变为-1。下图中就是上面的三种假设的抽象图。

h == 2,新增结点的情况:

 

不论abc的子树高度为多少,只要我们在a子树中新增一个结点就会造成需要做右单旋处理。

下面我们来进行旋转:

从图中可以看出经过旋转平衡因子已经变为了紫色的0,整棵树已经平衡。

我们让b变成6的左边,让6变为3的右边,这样就完成了右单旋。 

下面是右单旋的代码实现:

void RotateR(Node* parent) // 传入需要进行旋转的父结点(parent)即平衡因子为-2的接地那
{
	Node* subL = parent->_left; // 父结点的左孩子(subL)节点即平衡因子为-1的结点
	Node* subLR = subL->_right; // 需要进行旋转的平衡因子为-1的右孩子(subLR)

	parent->_left = subLR; // 让父结点的左子树变为其原来左子树的右子树
	if (subLR) // 这里需要注意父结点的左子树的右子树(subLR)是可能为空的,需要进行额外的判断
	{
		subLR->_parent = parent; // 链接subLR结点的parent指针
	}
	
	Node* ppNode = parent->_parent; // 这里需要将parent结点的父结点进行保存
	
	subL->_right = parent; 
	parent->_parent = subL;
	
	if (ppNode == nullptr) // 这里需要注意的是,parent结点是有可能为root结点的,如果为根节点,之前我们保存的ppNode结点就是为空,需要重新赋根结点,如果不是就需要进行重新的链接,重新链接时需要进行比较确定是ppNode的左子树还是右子树。然后将subL与ppNode互相赋值。
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		if (ppNode->_left == parent)
		{
			ppNode->_left = subL;
		}
		else // ppNode->_right = parent
		{
			ppNode->_right = subL;
		}
		subL->_parent = ppNode;
	}
	parent->_bf = subL->_bf = 0; // 这里就需要将parent、subL的平衡因子进行修正
}

新节点插入较高右子树的右侧 -- 左单旋

左单旋与右单旋是类似的这里就不再过多的介绍:

左单旋的旋转方式为将b变为3的右边,将3变为6的左边。

下面是左单旋的代码实现:

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

	parent->_right = sub_RL;
	if (subRL) {
		subRL->_parent = parent->_parent;
	}
	Node* ppNode = parent->_parent;
	
	subR->_left = parent;
	parent->_parent = subR;

	if (ppNode = nullptr) {
		_root = subR;
		subR->_parent = nullptr;
	}
	else {
		if (ppNode->_left == parent) {
			ppNode->_left = subR;
		}
		else {// ppNode->_right = parent
			ppNode->_right = subR;
		}
		subR->_parent = ppNode;
	}
	parent->_bf = subR->_bf = 0;
}

新节点插入较高左子树的右侧 -- 先左单旋再右单旋

下面我们再来展示一种左右双旋的抽象图:

在这个抽象图中ad是高度为h的子树,bc是高度为h-1的子树,新插入的结点在bc这两棵树中的任意一棵。同样下面我们来进行一些假设:

假设6号结点就是新增的结点,对平衡因子进行修正;

假设bc为空树,ad为只有一个结点的树,在6号结点的位置任意一颗子树新增节点,对平衡因子进行修正;

假设ad为一颗高度为2的子树,bc为高度为1的子树在6号结点的任意子树新增一个节点,对平衡因子进行调整;

......还有很多种的情况,但是同样这些的情况都有着这样的一个特点,就是平衡因子调整到最后一定会变为-2,其左孩子的平衡因子一定为1;下面就为这几种情况的模拟图:

 下面我们来进行旋转:

在这种情况下单一的左旋或者右旋已经无法起作用了,因此在这里需要我们进行左右双旋,此时的旋转可以对左单旋与右单旋进行复用,但是需要注意的是在旋转结束之后平衡因子的变化已经不再是全部变为0,需要重新进行赋值,下面分别是h==0、h==1、以及更加普遍的情况。

从上图中 可以看出当h == 0时平衡因子都修正为0,其余的情况当新增的子树时6号结点左子树中的结点,那么旋转之后的右子树的平衡因子就会变成1;当新增的子树时6号结点右子树中的结点,那么旋转之后的左子树的平衡因子就会变成-1;因此就需要将6号结点的平衡因子进行记录,方便处理旋转之后的平衡因子。

下面是左右双旋的代码实现:

void RotateLR(Node* parent) {
	Node* subL = parent->_left;
	Node* subLR = parent->_left->_right;
	
	int bf = subLR->_bf; // 这里需要记录subLR的平衡因子,用来确定后面旋转之后的平衡因子变化。

	RotateL(parent->_left);
	RotateR(parent);
	if (bf == 1) {
		subL->_bf = -1;
		parent->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == -1) {
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else {
		assert(false);
	}

新节点插入较高右子树的左侧 -- 先右单旋再左单旋

右左双旋与左右双旋跟之前的单旋一样也是类似的。下面就展示一下右左双旋的示意图,代码也是与左右双旋类似的:

当我们处于左右双旋的时候我们是在b或c的子树出进行结点的插入操作,假如我们在a子树进行插入的时候就会变为下图的形式:这与我们前面讲述的右旋是一致的,这样就只要使用之前我们学习的右旋即可,还有其他的情况也是可以互相转化的。因此我们就得到了四种旋转的情况。

总结:

假如以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为根的子树个高度降低,已经平衡,不需要再向上更新。

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

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

相关文章

【啃书C++Primer5】-c++有些理论基础需要了解,墙裂建议看看原书,有太多细节需要注意了

任何常用的编程语言都具备一组公共的语法特征&#xff0c;不同语言仅在特征的细节上有所区别。要想学习并掌握–种编程语言&#xff0c;理解其语法特征的实现细节是第一步。最基本的特征包括: 整型、字符型等内置类型变量&#xff0c;用来为对象命名 表达式和语句&#xff0c;…

10款最好的Photoshop替代软件

Photoshop作为一种老式的设计软件&#xff0c;在世界上享有很高的声誉&#xff0c;所以很多人说学习设计&#xff0c;第一反应是学习Photoshop&#xff0c;结果打开电脑发现&#xff1a;配置不够&#xff0c;预算不足&#xff0c;课程有点困难&#xff0c;因为这条路不起作用&a…

嵌入式系统入门基础知识分析(一)

目录 ​编辑 一、什么是嵌入式 二、嵌入式系统的组成 三、实时系统

薪资21K,在字节外包工作是一种什么样的体验...

我17年毕业于一个普通二本学校&#xff0c;电子信息工程学院&#xff0c;是一个很不出名的小本科。大学期间专业知识也没有去认真的学习&#xff0c;所以毕业的时候就随便找了一份工作&#xff0c;在一个小公司做功能测试。 记得那时候税前薪资大概是7k左右&#xff0c;因为是…

每日一练 | 华为认证真题练习Day46

1、在存在冗余的二层网络中&#xff0c;可以使用下列哪种协议避免出现环路&#xff1f; A. ARP B. STP C. UDP D. VRRP 2、静态MAC地址表在系统复位、接口板热插拔或接口板复位后&#xff0c;保存的表项不会丢失。 A. 对 B. 错 3、如下图所示&#xff0c;假设SWA的MAC地址…

web开发 处理多媒体文件

文章目录 图片格式gifpngjpgwebp图片格式对比 图片的使用方式图片的其它内容 视频格式转换 音频wavmp3转换 图片 html用img标签在网页上输出图片。 格式 图片的格式有很多种&#xff0c;常见如下 gif、jpg、png、webp。 gif gif 格式&#xff0c;常用于页面中的简单动画&a…

《链》接未来:力扣“复制带随机指针的链表”题解

本篇博客会讲解力扣“138. 复制带随机指针的链表”的解题思路&#xff0c;这是题目链接。 先来审题&#xff1a; 以下是输出示例&#xff1a; 以下是提示&#xff1a; 本题要实现复杂链表的深拷贝。复杂链表&#xff0c;是正常的单链表&#xff0c;每个结点中多存了一个指针…

【Vector VN1630/40 I/O应用】-2-信号发生器

案例背景(共5页精简)&#xff1a;该篇博客将告诉您&#xff1a; 将Vector VN1630A/VN1640A CAN/LIN Interface的I/O接口充当一个简易的“信号发生器”使用&#xff1a;高低电平(如TTL电平)和PWM波。用作信号发生器&#xff0c;唤醒ECU控制器&#xff08;硬件唤醒&#xff0c;如…

C语言参悟-循环控制

C语言参悟-循环控制 一、循环综述1. C语言的循环概念2. 循环条件3. 循环单元4. 循环中断 二、for 循环三、while 循环四、do while 循环 一、循环综述 下面只是我的对于C语言的认识拙见&#xff0c;可以多多提出批评建议&#x1f604; 计算机的诞生很大程度上就是为了来进行重…

即拼七人拼团系统开发模式,上帮下扶机制逻辑规则解析

即拼七人拼团模式是最近非常火爆的商业模式&#xff0c;它主要融合了二二复制和拼团两种模式玩法&#xff0c;在快速裂变团队的同时&#xff0c;还能提高用户活跃度和粘性。这个模式中最大的亮点&#xff0c;就是它的上帮下扶机制&#xff0c;今天就在这里详细说一下。 所谓上帮…

6.文本三剑客--sed

文章目录 文本三剑客sed介绍命令介绍打印内容删除替换插入分组 文本三剑客 sed 介绍 sed编辑器 sed是一种流编辑器&#xff0c;流编辑器会在编辑器处理数据之前基于预先提供的一组规则来 编辑数据流。 sed编辑器可以根据命令来处理数据流中的数据&#xff0c;这些命令要么…

Robot Techology

Two-Stage Grasping: A New Bin Picking Framework for Small Objects 摘要&#xff1a;本文提出了一种新的抓仓框架&#xff0c;两级抓取&#xff0c;旨在精确抓取杂乱的小物体。 在第一阶段进行了对象密度估计和粗糙抓取。在第二阶段&#xff0c;需要进行精细的分割、检测、…

怎么制作网站?手把手教你10个网站建设的步骤!

怎么制作网站&#xff1f;手把手教你10个网站建设的步骤&#xff01;网站建设需要进行10个步骤&#xff0c;首先要确定网站建设的目标&#xff0c;考虑用户、品牌信息和竞争对手等&#xff0c;避免方向错误。其次&#xff0c;绘制网站建设地图和原型&#xff0c;确定位置大小、…

2023-详解整个数据仓库建设体系

一、数据仓库的基本概念 数据仓库与数据库的区别 数据仓库分层架构 数据仓库元数据的管理 二、数仓建模方法 范式建模法 维度建模法 实体建模法 三、维度建模 维度建模中表的类型 维度建模三种模式 维度建模过程 四、实际业务中数仓分层 数据源层ODS 数据明细层DW…

老域名扫描软件-老域名采集挖掘工具

老域名挖掘软件 老域名挖掘软件是一种可以帮助用户发现已过期或未续费的老域名的工具。以下是该软件主要特点&#xff1a; 大数据分析&#xff1a;该软件通过大数据分析技术&#xff0c;深度挖掘互联网上的闲置老域名&#xff0c;发现可用的未续费或已过期域名&#xff0c;从…

【面试题】面试官:说说你对 CSS 盒模型的理解

前言 CSS 盒模型是 CSS 基础的重点难点&#xff0c;因此常被面试官们拿来考察候选人对前端基础的掌握程度&#xff0c;这篇文章将对 CSS 盒模型知识点进行全面的梳理。 我们先看个例子&#xff1a;下面的 div 元素的总宽度是多少呢&#xff1f; js <!DOCTYPE html> &…

第七章结构性模式—适配器模式

文章目录 适配器模式解决的问题概念结构 类适配器模式对象适配器模式接口适配器模式应用场景JDK 源码 - Reader 与 InputStream 结构型模式描述如何将类或对象按某种布局组成更大的结构&#xff0c;有以下两种&#xff1a; 类结构型模式&#xff1a;采用继承机制来组织接口和类…

AUTOSAR-MemIf

1、MemIf的功能 从AUTOSAR的架构图中可以看出&#xff0c;MemIf(Memory Abstraction Interface)模块位于Memory Hardware Abstraction。  一方面&#xff0c;NvM使用MemIf提供的接口访问NV memory&#xff08;NV memory分为两种&#xff1a;Flash和EEPROM&#xff0c;位于MCAL…

APP图标尺寸规范一文了解清楚

在进行图标设计前&#xff0c;熟知手机 app 图标尺寸规范&#xff0c;能更好地去针对不同平台设计出更极致的图标。当前智能手机系统主要以 iOS 及 Android 为主&#xff0c;APP 图标是产品给用户的第一印象&#xff0c;图标视觉设计的美感与吸引力&#xff0c;与用户是否选择下…

分子模拟力场

分子模拟力场 AMBER力场是在生物大分子的模拟计算领域有着广泛应用的一个分子力场。开发这个力场的是Peter Kollman课题组&#xff0c;最初AMBER力场是专门为了计算蛋白质和核酸体系而开发的&#xff0c;计算其力场参数的数据均来自实验值&#xff0c;后来随着AMBER力场的广泛…