【C++】平衡二叉树(AVL树)的实现

news2024/11/26 4:29:44

目录

  • 一、AVL树的概念
  • 二、AVL树的实现
    • 1、AVL树的定义
    • 2. 平衡二叉树的插入
      • 2.1 按照二叉排序树的方式插入并更新平衡因子
      • 2.2 AVL树的旋转
        • 2.2.1 新节点插入较高左子树的左侧(LL平衡旋转)
        • 2.2.2 新节点插入较高右子树的右侧(RR平衡旋转)
        • 2.2.3 新节点插入较高左子树的右侧(LR平衡旋转)
        • 2.2.4 新节点插入较高右子树的左侧(RL平衡旋转)
        • 2.2.5 总结
    • 3 平衡二叉树的删除(了解即可)
    • 4 平衡二叉树的验证
  • 三、平衡二叉树的效率分析

一、AVL树的概念

二叉排序树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
为了避免树的高度增长过快,降低二叉排序树的性能,规定在插入和删除结点时,要保证任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树,也称AVL树。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)在这里插入图片描述

二、AVL树的实现

1、AVL树的定义

AVL树结点的定义:

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_bf(0)
	{}
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;		// 使用三叉链方便后续处理,但要记得维护
	pair<K, V> _kv;					// 保存键值对
	int _bf;						// 平衡因子
};

2. 平衡二叉树的插入

2.1 按照二叉排序树的方式插入并更新平衡因子

AVL树就是在二叉排序树的基础上加上了平衡因子,因此AVL树也可以看成是二叉排序树。那么AVL树的插入过程可以分为两步:
(1) 按照二叉排序树的方法插入新结点
(2) 调整结点的平衡因子

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 = new Node(kv);
	if (kv.first < parent->_kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
	cur->_parent = parent;
	// 新结点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否
	// 破坏了AVL树的平衡性
	while (parent)
	{
		/*
		cur插入后,parent的平衡因子一定需要调整,在插入之前,parent
		的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
 			  1. 如果cur插入到parent的左侧,只需给parent的平衡因子-1即可
 			  2. 如果cur插入到parent的右侧,只需给parent的平衡因子+1即可
		 */
		if (parent->_left == cur)
		{
			--parent->_bf;
		}
		else
		{
			++parent->_bf;
		}
		/*
		 此时:parent的平衡因子可能有三种情况:0,正负1, 正负2
 			  1. 如果parent的平衡因子为0,说明插入之前parent的平衡因子为正负1,插入后被调整
		成0,此时满足AVL树的性质,插入成功
 			  2. 如果parent的平衡因子为正负1,说明插入前parent的平衡因子一定为0,插入后被更
		新成正负1,此时以parent为根的树的高度增加,需要继续向上更新
 			  3. 如果parent的平衡因子为正负2,则parent的平衡因子违反平衡树的性质,需要对其进
		行旋转处理
		 */
		if (0 == parent->_bf)
		{
			break;
		}
		else if (1 == parent->_bf || -1 == parent->_bf)
		{
			cur = cur->_parent;
			parent = parent->_parent;
		}
		else if (2 == parent->_bf || -2 == parent->_bf)
		{
			// 旋转处理
		}
		else
		{
			// 如果平衡因子不是以上几种情况,说明代码逻辑错误
			assert(false);
		}
	}
	return true;
}

2.2 AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:LL平衡旋转(右旋),RR平衡旋转(左旋),LR平衡旋转(先左旋后右旋),RL平衡旋转(先右旋后左旋)

2.2.1 新节点插入较高左子树的左侧(LL平衡旋转)

在这里插入图片描述
上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。

在旋转过程中,有以下几种情况需要考虑:

  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树
    如果是根节点,旋转完成后,要更新根节点
    如果是子树,可能是某个节点的左子树,也可能是右子树
void RotateR(Node* parent)
{
	// subL:parent的左孩子
	// subLR:parent的左孩子的右孩子,注意:该点可能不存在
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	
	subL->_right = parent;
	parent->_left = subLR;
	Node* ppnode = parent->_parent;		// 记录parent的父结点,用于连接新的子树

	parent->_parent = subL;

	if (subLR)
	{
		subLR->_parent = parent;
	}
	if (ppnode == nullptr)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else 
	{
		if (ppnode->_left == parent)
		{
			ppnode->_left = subL;
		}
		else
		{
			ppnode->_right = subL;
		}
		subL->_parent = ppnode;
	}
	// 根据调整后的结构更新部分节点的平衡因子
	subL->_bf = parent->_bf = 0;
}
2.2.2 新节点插入较高右子树的右侧(RR平衡旋转)

在这里插入图片描述
具体实现参考右旋即可。

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

	subR->_left = parent;
	parent->_right = subRL;
	Node* ppnode = parent->_parent;		// 记录parent的父结点

	parent->_parent = subR;

	if (subRL)
	{
		subRL->_parent = parent;
	}
	if (ppnode == nullptr)
	{
		_root = subR;
		_root->_parent = nullptr;
	}
	else
	{
		if (ppnode->_left == parent)
		{
			ppnode->_left = subR;
		}
		else
		{
			ppnode->_right = subR;
		}
		subR->_parent = ppnode;
	}
	parent->_bf = subR->_bf = 0;
}
2.2.3 新节点插入较高左子树的右侧(LR平衡旋转)

在这里插入图片描述
将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。

void RotateLR(Node* parent)
{
	// subL:parent的左孩子
	// subLR:parent的左孩子的右孩子,注意:该点可能不存在
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	// 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子
	int bf = subLR->_bf;
	RotateL(parent->_left);
	RotateR(parent);

	if (1 == bf)
	{
		subL->_bf = -1;
	}
	else if (-1 == bf)
	{
		parent->_bf = 1;
	}
}
2.2.4 新节点插入较高右子树的左侧(RL平衡旋转)

在这里插入图片描述
参考右左双旋。

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;
	RotateR(parent->_right);
	RotateL(parent);

	if (1 == bf)
	{
		parent->_bf = -1;
	}
	else if (-1 == bf)
	{
		subR->_bf = 1;
	}
}
2.2.5 总结

假如以parent为根的子树不平衡,即parent的平衡因子为2或者-2,分以下情况考虑:

  1. parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR
    当subR的平衡因子为1时,执行左单旋
    当subR的平衡因子为-1时,执行右左双旋
  2. parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL
    当subL的平衡因子为-1是,执行右单旋
    当subL的平衡因子为1时,执行左右双旋

旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新。

3 平衡二叉树的删除(了解即可)

因为AVL树也是二叉排序树,可按照二叉排序树的方式将节点删除,然后再更新平衡因子,只不错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
平衡二叉树删除操作的具体步骤:

  1. 先按照二叉排序树的方式删除结点
  2. 一路向上找到最小不平衡子树,找不到就结束
  3. 找最小不平衡子树下,最高的儿子和孙子
  4. 根据孙子的位置,调整平衡
    • 孙子在LL:右单旋
    • 孙子在RR:左单旋
    • 孙子在LR:先左旋再右旋
    • 孙子再RL:先右旋再左旋
  5. 如果不平衡向上传导,继续第二步
    • 对最小不平衡子树的旋转可能导致树变矮,从而导致上层祖先不平衡

4 平衡二叉树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    • 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    • 节点的平衡因子是否计算正确
// 求二叉树的高度
int _Height(Node* root)
{
	if (root == nullptr)
	{
		return 0;
	}
	int leftH = _Height(root->_left);
	int rightH = _Height(root->_right);

	return leftH > rightH ? leftH + 1 : rightH + 1;
}
// 验证平衡树
bool _Isbalance(Node* root)
{
	if (root == nullptr)
	{
		return true;
	}

	int leftH = _Height(root->_left);
	int rightH = _Height(root->_right);
	if (rightH - leftH != root->_bf)
	{
		cout << root->_kv.first << "结点平衡因子异常" << endl;
		return false;
	}
	return rightH - leftH < 2
		&& _Isbalance(root->_left)
		&& _Isbalance(root->_right);
}

三、平衡二叉树的效率分析

在平衡二叉树上进行查找的过程与二叉排序树相同。因此,在查找过程中,进行关键字的比较次数不超过树的深度。假设以 n h n_h nh表示深度为h的平衡二叉树中含有的最少结点数。 n 0 = 0 , n 1 = 1 , n 2 = 2 n_0=0,n_1=1,n_2=2 n0=0,n1=1,n2=2,并且有 n h = n h − 2 + n h − 1 + 1 n_h=n_{h-2}+n_{h-1}+1 nh=nh2+nh1+1含有n个结点的平衡二叉树的最大深度为 O ( l o g 2 n ) O(log_2n) O(log2n),因此平均查找效率为 O ( l o g 2 n ) O(log_2n) O(log2n)
但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

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

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

相关文章

【计算机毕业设计】204基于微信小程序疫情期间学生请假与销假系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

windows使用curl命令出现乱码的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

第3章 小功能大用处-发布订阅

Redis提供了基于“发布/订阅”模式的消息机制&#xff0c;此种模式下&#xff0c;消息发布者和订阅者不进行直接通信&#xff0c;发布者客户端向指定的频道&#xff08;channel&#xff09;发布消息&#xff0c;订阅该频道的每个客户端都可以收到该消息。 命令&#xff1a;Red…

【CPP】插入排序、希尔排序

目录 1.插入排序1.1直接插入排序简介代码分析 1.2直接插入对比冒泡排序简介代码对比分析(直接插入排序与冒泡的复杂度效率区别) 1.3希尔排序简介代码分析 1.插入排序 基本思想&#xff1a;把一个待排数字按照关键码值插入到一个有序序列中&#xff0c;得到一个新的有序序列。 …

高校新闻头条系统

摘 要 随着互联网技术的快速发展&#xff0c;网络几乎成为了人们搜集信息和交流沟通最方便、快捷的通道&#xff0c;科技创新一直在影响着人们的生活&#xff0c;人们的衣食住行也在不断变化&#xff0c;与此同时&#xff0c;也大大改变了人们获取信息的方式&#xff0c;人们获…

甘肃旅游服务平台的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;公告信息管理&#xff0c;景点管理&#xff0c;酒店管理&#xff0c;基础数据管理&#xff0c;美食管理 前台账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0…

kafka第二篇——主题创建,主题分区副本分配策略

目录 主题创建 引入 回顾主题&#xff0c;分区&#xff0c;副本关系 代码 主题分区副本分配策略 底层逻辑分析 主题创建 引入 为啥才开始在idea上跑代码&#xff0c;topic都没创建出来&#xff0c;就直接跑出来了? 这是因为AutoTopicCreationManger参数可以自动创建主题…

首个AI高考全卷评测结果出分,大模型“考生”表现如何?

内容提要 大部分大模型“考生”语文、英语科目表现良好&#xff0c;但在数学方面还有待加强。阅卷老师点评&#xff0c;在语文科目上&#xff0c;对于语言中的一些“潜台词”&#xff0c;大模型尚无法完全理解。在数学科目上&#xff0c;大模型的主观题回答相对凌乱&#xff0…

关于运用人工智能帮助自己实现英语能力的有效提升?

# 实验报告 ## 实验目的 - 描述实验的目标&#xff1a;自己可以知道&#xff0c;自己的ai学习方法是否可以有效帮助自己实现自己的学习提升。 预期结果&#xff1a;在自己利用科技对于自己进行学习的过程中&#xff0c;自己的成长速度应该是一个幂指数的增长 ## 文献回顾 根据…

LabVIEW开发电机故障监测系统

开发了一套基于LabVIEW的智能电机故障监测系统&#xff0c;通过实时监测和分析电机运行参数&#xff0c;实现故障预测和诊断。系统集成了振动传感器、温度传感器、数据采集模块和分析模块&#xff0c;利用RS-485通信总线和Modbus协议确保数据的高精度实时传输&#xff0c;故障预…

Bootloader -- U-Boot 介绍

Bootloader -- U-Boot 介绍 1 介绍1.1 概述1.2 知名 BootloaderLILO (Linux Loader)GRUB (GNU GRand Unified Bootloader)LoadlinROLO (Rockbox Loader)EtherbootLinuxBIOS (现在叫 coreboot)BLOBU-BootRedBoot 1.3 BootLoader 和 Monitor 区别1.4 U-Boot 的源码结构1.5 U-Boot…

Keil MDK生成LIB库以及使用LIB库

一.keil下lib静态库的使用具有以下优点&#xff1a; 1.封装源代码&#xff0c;对外不开放&#xff0c;提高保密性。 2.将标准功能函数封装成库&#xff0c;提高代码的复用性。 3.使用库开发&#xff0c;简化开发流程&#xff0c;提高开发效率。 4.系统调用库函数&#xff0…

无人直播不封号美女舞团3.0 多重防非操作(教程+素材+工具)

在这个数字化的时代&#xff0c;直播已经成为了一种新的商业模式。然而&#xff0c;如何打造一个24小时自动循环播放的直播间&#xff0c;并通过此获得收益&#xff0c;却是许多人面临的挑战。本文将介绍如何通过使用OBS和咩播软件&#xff0c;创建一个能够吸引人们进入直播间并…

VMware vSphere Bitfusion 4.5.4 - 面向 AI 和 ML 应用提供弹性基础架构

VMware vSphere Bitfusion 4.5.4 - 面向 AI 和 ML 应用提供弹性基础架构 请访问原文链接&#xff1a;VMware vSphere Bitfusion 4.5.4 - 面向 AI 和 ML 应用提供弹性基础架构&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org VM…

VBA学习(18):VBA制作任意工作表均可使用的聚光灯

在需要制作聚光的工作簿&#xff0c;按<ALTF11>组合键&#xff0c;打开VBE编辑器。在右侧[工程资源管理器窗格]选中ThisWorkbook模块&#xff0c;将以下代码复制粘贴到该模块的代码窗口。 Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target …

软件缺陷及JIRA工具

一、软件缺陷及跟踪流程 1&#xff0c;软件缺陷信息 案例 &#xff08;1&#xff09;缺陷报告的基本内容 缺陷的标题 预置条件 重现步骤 期望结果 实际结果 &#xff08;2&#xff09;软件缺陷的状态 新建 打开 修复 关闭 &#xff08;3&#xff09;软件缺陷的严重程度 …

NeRF从入门到放弃3: EmerNeRF

https://github.com/NVlabs/EmerNeRF 该方法是Nvidia提出的&#xff0c;其亮点是不需要额外的2D、3Dbox先验&#xff0c;可以自动解耦动静field。 核心思想&#xff1a; 1. 动、静filed都用hash grid编码&#xff0c;动态filed比静态多了时间t&#xff0c;静态的hash编码输入是…

Python文件与面向对象知识点

目录 文件的基本概念 文件的读取 文件的追加 文件的写入 with语句 知识总结 面向对象的基本概念 类和实例 对象的属性和方法 类属性与方法 面向对象的三大特性 知识总结 文件的基本概念 文件的读取 文件的追加 文件的写入 with语句 知识总结 面向对象的基本概念 …

windows如何查看硬盘类型(查看磁盘类型)(查看是固态硬盘ssd还是机械硬盘hdd)(Windows优化驱动器——媒体类型)

文章目录 方法&#xff1a;使用Windows优化驱动器1、在任务栏搜索框中输入“优化驱动器”并打开它。2、在优化驱动器的窗口中&#xff0c;查看每个驱动器旁边的“媒体类型”。3、如果列出的是“固态驱动器”&#xff0c;那么它是SSD&#xff1b;如果是“硬盘驱动器”&#xff0…

新手充电-boost升压电路解析

1.boost升压电路解析 本篇文章从充放电两个方面来对Boost电路的原理进行了讲解。并在最后补充了一些书本上没有的知识,整体属于较为新手向的文章,希望大家在阅读过本篇文章之后,能对Boost电路的基本原理有进一步了解。 Boost电路是一种开关直流升压电路,它能够使输出电压高…