【C++】详解AVL树——平衡二叉搜索树

news2024/11/26 7:27:53

个人主页:东洛的克莱斯韦克-CSDN博客

祝福语:愿你拥抱自由的风

目录

二叉搜索树

AVL树概述

平衡因子

旋转情况分类

左单旋

右单旋

左右双旋

右左双旋

AVL树节点设计

AVL树设计

详解单旋

左单旋

右单旋

详解双旋

左右双旋

平衡因子情况如下

右左双旋

平衡因子情况如下


二叉搜索树

【C++】详解二叉搜索树-CSDN博客

AVL树概述

平衡树:左子树的高度等于右子树的高度

不平衡树:左子树的高度不等于等于右子树的高度

二叉搜索树很难是一颗平衡树。

对二叉树进行插入和删除的操作,或插入大量的数据不够随机,都会是使二叉搜索树不够平衡。

极端情况下,二叉树会退化成类似链表的结构,那么二叉搜索树查询数据的效率荡然无存。

在二叉树的基础上加入平衡的概念就是平衡二叉搜索树,也叫AVL树

AVL树也不是一颗绝对的平衡树,AVL树的平衡是相对的,它允许左子树和右子树的高度为 1 ,但不能超过 1

平衡是相对的很好理解,因为一个父亲节点最多只能有两个孩子节点,而数据又是一个一个插入的,所以一定会出现左子树和右子树高度差为 1 的情况。

B树可达到绝对平衡,因为B树是多叉结构——一个父亲节点有多个孩子节点

如果左子树和右子树的高度差为 2 就视为打破平衡

如果打破平衡,就需要通过旋转这一操作让左右子树的高度差小于等于 1 。

AVL树是保持一种相对平衡的状态,而不是绝对平衡。那么AVL树搜索数据的效率只能是接近O(logN)

AVL树只是保证了搜索效率的下限,而不是提高了上限

平衡因子

平衡因子这一概念并不是AVL树所必备的——从代码实现的角度来说,如果不加入平衡因子的概念理解起来会比较抽象。

平衡因子:让每个节点存一个整型,该整形值的大小等于右子树的高度减左子树的高度

平衡因子等于 0 左右子树平衡

平衡因子等于 1左右子树相对平衡,右树偏高

平衡因子等于 -1 :左右子树相对平衡,左树树偏高

平衡因子等于 2 -2左右子树不平衡

平衡因子的更新:

插入父亲节点的右边平衡因子加加,插入父亲节点的右边平衡因子减减

父亲节点更新后的平衡因子等于 1 或 -1 ,需要不断往上(溯源)更新,直到父亲节点的平衡因子为 0 或 更新至整棵树的根节点就停止更新

如果父亲节点的平衡因子为 2 或 -2 时,需要对这棵子树旋转,旋转后更新平衡因子

示例

旋转情况分类

旋转分为:

左单旋 右单旋  左右双旋  右左双旋

左单旋

:新节点插入较高右子树的右侧

具象图:

抽象图:

那么左单旋是怎么旋的呢?核心步骤为:

设父亲节点为:fathernode 孩子节点为:cur

cur的左孩子成为fathernode的右孩子,

再让fathernode成为cur的左孩子。

如下示意图

右单旋

:新节点插入较高左子树的左侧

具象图:

抽象图:

那么右单旋是怎么旋的呢?核心步骤为:

设父亲节点为:fathernode 孩子节点为:cur

cur的右孩子成为fathernode的左孩子,

再让fathernode成为cur的右孩子

如下示意图:

左右双旋

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

左右双旋的核心步骤为:

设父亲节点为:fathernode 

父亲的左孩子节点为:fathernodeL

父亲的左孩子节点的右孩子节点的为fathernodeLR

先让fathernodeL左单旋,再让fathernodeLR进行右单旋

这里小编直接上抽象图:

右左双旋

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

设父亲节点为:fathernode 

父亲的 右孩子节点为:fathernodeR

父亲的右孩子节点的左孩子节点的为fathernodeRL

先对fathernodeR进行右单旋,再对fathernode进行左单旋。

示意图:

AVL树节点设计

【C++】详解C++的模板-CSDN博客

AVL树的节点需要三个指针,分别指向左孩子节点,右孩子节点,父亲节点。指向父亲节点的指针是为了能溯源更新平衡因子。

需要一个整型存储平衡因子,平衡因子在构造函数的初始化列表中初始化为 0,因为新节点左右孩子都为空。

template <class K>
class AVLTreeNode
{
public:

	AVLTreeNode(const K& key) //构造函数
		:_key(key)
		, _left(nullptr)
		, _right(nullptr)
		, _FatherNode(nullptr)
		, _bf(0)
	{

	}

	K _key; //键值   

	AVLTreeNode<K>* _left;//左孩子
	AVLTreeNode<K>* _right;//右孩子
	AVLTreeNode<K>* _FatherNode;//父亲  

	int _bf;//平衡因子

};

AVL树设计

template <class K>
class AVLTree
{
	typedef AVLTreeNode<K> node; 

	node* _root;

public:

	AVLTree()  //构造函数,初始化为空树
		:_root(nullptr)
	{

	}




	bool Insert(const K& key)//插入元素
	{
//
		if (nullptr == _root) //是否是空树
		{
			_root = new node(key);  
			return true;
		}
//
		node* cur = _root;
		node* fathernode = nullptr;

		while (cur)  //查找插入的位置,如果树中已经有要插入的值,则插入失败,
		{
			if (cur->_key < key)
			{
				fathernode = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				fathernode = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}

		}


			cur = new node(key); //新插入节点 

			if (fathernode->_key < cur->_key) //判断新节点应该是左孩子还是右孩子
			{
				fathernode->_right = cur;
				cur->_FatherNode = fathernode;

			}
			else
			{
				fathernode->_left = cur;
				cur->_FatherNode = fathernode;
			}
			//
			
			while (fathernode)//更新平衡因子
			{

			if (cur == fathernode->_left)
			{
				fathernode->_bf--;
			}
			else  if (cur == fathernode->_right)
			{
				fathernode->_bf++;
			}


			//
			if (fathernode->_bf == 0)
			{
				// 更新结束
				break;
			}

			else if (fathernode->_bf == 1 || fathernode->_bf == -1)
			{
				// 继续往上更新
				cur = fathernode;
				fathernode = fathernode->_FatherNode;
			}
			else if (fathernode->_bf == 2 || fathernode->_bf == -2)
			{
				// 子树不平衡了,需要旋转
				if (fathernode->_bf == 2 && cur->_bf == 1)
				{
					RevolveLeft(fathernode);//左单旋
				}
				else if (fathernode->_bf == -2 && cur->_bf == -1)
				{
					RevolveRight(fathernode);//右单旋
				}
				else if (fathernode->_bf == 2 && cur->_bf == -1)
				{
					RevolveRightLeft(fathernode); //右左双旋   
					
				}
				else if (fathernode->_bf == -2 && cur->_bf == 1)
				{
					RevolveLeftRight(fathernode);//左右双旋
				}
				else
				{
					assert(false);   //平衡因子出问题了
				}
				
				break;
			}
		

	}

	return true;
	}

}

下面通过代码的细节来深入理解旋转

详解单旋

左单旋

完整代码如下

void RevolveLeft(node *& fathernode)//左单旋      
{
	node* cur = fathernode->_right; //父亲节点的右孩子

	fathernode->_right = cur->_left; //更改指向关系

	if (cur->_left != nullptr) //特殊情况
		cur->_left->_FatherNode = fathernode;//更改指向关系

	cur->_FatherNode = fathernode->_FatherNode;//更改指向关系

	if (fathernode->_FatherNode != nullptr) //为空是特殊情况,
	{

		if (fathernode->_FatherNode->_right == fathernode)
		{
			fathernode->_FatherNode->_right = cur;//更改指向关系
		}
		else
		{
			fathernode->_FatherNode->_left = cur;//更改指向关系
		}

	}

	cur->_left = fathernode;//更改指向关系

	fathernode->_FatherNode = cur;//更改指向关系

	fathernode->_bf = 0; //更新平衡因子
	cur->_bf = 0;

}

处理指向关系时,一定不要忘了更改父亲的指向关系

经过左单旋之后,父亲节点和右孩子节点的平衡因子都为 0 ,可参考上文图示。

特殊情况如下,如果不处理特殊情况,程序很容易崩溃

右单旋

void RevolveRight(node *& fathernode) //右单旋
{
	node* cur = fathernode->_left; //父亲节点的左节点

	fathernode->_left = cur->_right;//更新指向关系

	if (cur->_right != nullptr) //特殊情况
		cur->_right->_FatherNode = fathernode;//更新指向关系

	cur->_FatherNode = fathernode->_FatherNode;//更新指向关系

	if (fathernode->_FatherNode != nullptr)//特殊情况
	{

		if (fathernode->_FatherNode->_right == fathernode)
		{
			fathernode->_FatherNode->_right = cur;//更新指向关系
		}
		else
		{
			fathernode->_FatherNode->_left = cur;//更新指向关系
		}

	}


	cur->_right = fathernode;//更新指向关系

	fathernode->_FatherNode = cur;//更新指向关系

	fathernode->_bf = 0;//更新平衡因子
	cur->_bf = 0;
}

详解双旋

左右双旋

左右双旋只需复用左单旋和右单旋即可,但平衡因子的更新却比较麻烦

完整代码如下

	void RevolveLeftRight(node *& fathernode)//左右双旋    
	{
		node* fathernodeL = fathernode->_left; //父亲节点的左孩子节点
		node* fathernodeLR = fathernodeL->_right;//父亲节点的左孩子节点的右孩子节点

		int bf = fathernodeLR->_bf; //保存平衡因子,实际是为了判断是插入了fathernodeLR左边还是右边还是fathernodeLR本身插入

		RevolveLeft(fathernodeL);
		RevolveRight(fathernode);

//更新平衡因子
		if (bf == 0)
		{
			fathernode->_bf = 0;
			fathernodeL->_bf = 0;
			fathernodeLR->_bf = 0;
		}
		else if (bf == -1)
		{
			fathernode->_bf = 1;
			fathernodeL->_bf = 0;
			fathernodeLR->_bf = 0;
		}
		else if (bf == 1)
		{
			fathernodeL->_bf = -1;
			fathernode = 0;
			fathernodeLR = 0;
		}
		else
		{
			assert(false);
		}


	}

平衡因子情况如下

右左双旋

完整代码如下

	void RevolveRightLeft(node *& fathernode) //右左双旋 
	{
		node* fathernodeR = fathernode->_right; 
		node* fathernodeRL = fathernodeR->_left;

		int bf = fathernodeRL->_bf;

		RevolveRight(fathernodeR);
		RevolveLeft(fathernode);
		if (bf == 0)
		{
			fathernode->_bf = 0;
			fathernodeR->_bf = 0;
			fathernodeRL->_bf = 0;
		}
		else if (bf == 1)
		{
			fathernode->_bf = -1;
			fathernodeR->_bf = 0;
			fathernodeRL->_bf = 0;
		}
		else if (bf == -1)
		{
			fathernodeR->_bf = 1;
			fathernode->_bf = 0;
			fathernodeRL->_bf = 0;
		}
		else
		{
			assert(false); 
		}
	}
	

平衡因子情况如下

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

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

相关文章

远程工作的数据安全挑战和解决策略

随着远程工作的普及&#xff0c;数据安全面临了前所未有的挑战。企业在应对这些挑战时&#xff0c;必须采取切实有效的策略来保护敏感信息。以下是远程工作数据安全的主要挑战和相应的解决策略&#xff1a; 数据安全挑战 设备丢失或被盗&#xff1a;员工在外工作时&#xff0c…

从 0 开始本地部署大语言模型

1、准备 ● Ollama&#xff1a;ollama.com ● Docker&#xff1a;https://docs.openwebui.com/ 2、下载 Ollama 进入 Ollama 官网&#xff0c;点击 Download 。 下载完成后&#xff0c;双击安装&#xff0c;什么都不需要勾选&#xff0c;直接下一步即可。安装完成&#xf…

算法之堆排序

堆排序是一种基于比较的排序算法&#xff0c;通过构建二叉堆&#xff08;Binary Heap&#xff09;&#xff0c;可以利用堆的性质进行高效的排序。二叉堆是一个完全二叉树&#xff0c;可以有最大堆和最小堆两种形式。在最大堆中&#xff0c;父节点的值总是大于或等于其子节点的值…

【TB作品】stm32单片机读取DS2401程序

DS2401是由Analog Devices公司生产的一种硅序列号芯片&#xff0c;它提供了一个绝对唯一的64位ROM识别码&#xff0c;用于确保可追溯性。以下是对DS2401器件的分析&#xff1a; 特点和优势&#xff1a; 唯一性&#xff1a;每个DS2401芯片都有一个独一无二的64位注册码&#x…

Redis机制-Redis缓存穿透,击穿,雪崩理解等问题的理解和学习

目录 一 缓存穿透问题 二 缓存击穿问题 三 缓存雪崩问题&#xff1a; 图1 正常的Redis缓存流程 一 缓存穿透问题 我们都知道Redis是一个存储键值对的非关系型数据库&#xff0c;那么当用户进行查询的时候&#xff0c;势必会从前端发起请求&#xff0c;从而数据从Redis缓存…

MySQL笔记第三天(从小白到入门)

文章目录 MySQL笔记SQL语言介绍数据库系统关系型数据库非关系型数据库SQL和数据库系统的关系数据库系统架构 MySQL的介绍概念MySQL的版本 MySQL的DDL操作-重点基本数据库操作基本表操作 MySQL的DML操作-重点insert-插入数据update-更新数据delete-删除数据 MySQL的约束-了解概述…

RabbitMQ .NET

setup rabbitmq docker run --namerabbit -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USERadmin -e RABBITMQ_DEFAULT_PASSxxx -d rabbitmq:management ip:15672 login nuget RabbitMQ.Client Send //1.1.实例化连接工厂 var factory new ConnectionFactory() …

WordPress搭建流程

1. 简介 WordPress 是一个 PHP 编写的网站制作平台。WordPress 本身免费,并且拥有众多的主题可以使用,适合用于搭建个人博客、公司官网、独立站等。 2. 环境准备 2.1 WordPress 下载 WordPress 可以在 Worpress中文官网 下载(如果后续要将后台调成中文的话,一定要从中文…

虚拟局域网(VLAN)

关键词&#xff1a;veth、vlan、bridge、iptables、nat、tcpdump、icmp、cidr、arp、路由表、计算机网络协议栈 前言 在过去的几十年里&#xff0c;互联网发展得非常快。许多新兴技术迅速崛起&#xff0c;也有不少曾经的主流技术被淘汰。然而&#xff0c;有些技术因为其基础性…

sqlserver 创建表,列及表,列描述

-- 创建表 CREATE TABLE Employees (EmployeeID INT PRIMARY KEY,EmployeeName NVARCHAR(100),EmployeeEmail NVARCHAR(100) );-- 为表添加描述 EXEC sp_addextendedproperty name NMS_Description, value N员工信息表, level0type NSchema, level0name dbo, level1type N…

栈和队列的经典例题,LeetCode 括号匹配问题;栈实现队列;队列实现栈;队列带环问题

1.前序 又有很久没有更新文章了&#xff0c;这次带你们手撕几道基础题&#xff1b;真的就和康纳吃饭一样简单&#xff01;&#xff01;&#xff01; 如果还不会队列和栈的可以去看看之前写的博客&#xff1b; 栈的实现 队列概念以及实现 <- 快速传送 目录 1.前序 …

优化css样式的网站

一、按钮的css样式 https://neumorphism.io/#e0e0e0https://neumorphism.io/#e0e0e0 二、渐变样式 Fresh Background Gradients | WebGradients.com &#x1f48e;Come to WebGradients.com for 180 beautiful linear gradients in CSS3, Photoshop and Sketch. This collect…

Redisson-分布式锁单Redis节点模式

Redisson-分布式锁单Redis节点模式 为什么要用分布式锁&#xff1f; 使用分布式锁的主要目的是为了解决多线程或多进程并发访问共享资源时可能出现的竞争条件和数据一致性问题。举一些实际场㬌&#xff1a; 数据库并发控制&#xff1a;在分布式系统中&#xff0c;多个节点同…

【CTF Web】CTFShow web4 Writeup(SQL注入+PHP+字符型注入)

web4 1 管理员阿呆又失败了&#xff0c;这次一定要堵住漏洞 解法 注意到&#xff1a; <!-- flag in id 1000 -->拦截很多种字符&#xff0c;连 select 也不给用了。 if(preg_match("/or|\-|\\\|\/|\\*|\<|\>|\!|x|hex|\(|\)|\|select/i",$id)){die(&q…

抖音运营_如何做出优质的短视频

目录 一 短视频内容的构成 1 图像 2 字幕 3 声音 4 特效 5 描述 6 评论 二 短视频的热门类型 1 颜值圈粉类 2 知识教学类 3 幽默搞笑类 4 商品展示类 5 才艺技能类 6 评论解说类 三 热门短视频的特征 1 产生共鸣 2 正能量 3 紧跟热点话题 4 富有创意 四 短视…

计算机网络套接字知识(非常详细)从零基础入门到精通

本节重点 认识IP地址, 端口号, 网络字节序等网络编程中的基本概念; 学习socket api的基本用法; 一、预备知识 1.理解源IP地址和目的IP地址 ⭐在IP数据包头部中&#xff0c;有两个IP地址&#xff0c;分别叫做源IP地址和目的IP地址。 思考: 我们光有IP地址就可以完成通信了…

spring boot 集成mongodb

引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId><version>2.2.0.RELEASE</version></dependency>配置db: spring:data:mongodb:host: 127.0.…

【C++高阶(一)】继承

目录 一、继承的概念 1.继承的基本概念 2.继承的定义和语法 3.继承基类成员访问方式的变化 ​编辑 4.总结 二、基类和派生类对象赋值转换 三、继承中的作用域 四、派生类的默认成员函数 1.派生类中的默认构造函数 2.派生类中的拷贝构造函数 3.派生类中的移动构造函数…

Kubeadm安装部署k8s集群、踩坑日常

背景 ​ Docker是一个非常流行的容器化平台&#xff0c;它可以让我们方便构建、打包、发布和运行容器化应用程序。但是&#xff0c;在生产环境中&#xff0c;我们可能需要处理成百上千个容器&#xff0c;需要更好的管理这些容器&#xff0c;这就是Kubernetes(K8S)的用武之地。…

vue15:记事本vue指令案例

效果图&#xff1a; vue指令 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>帅临记事本</…