【数据结构】AVL树——平衡二叉搜索树

news2024/12/29 4:14:52

个人主页:东洛的克莱斯韦克-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/1710982.html

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

相关文章

基于微信小程序+ JAVA后端实现的【微信小程序跑腿平台】设计与实现 (内附设计LW + PPT+ 源码+ 演示视频 下载)

项目名称 项目名称&#xff1a; 《微信小程序跑腿平台的设计与实现》 项目技术栈 该项目采用了以下核心技术栈&#xff1a; 后端框架/库&#xff1a; Java, SSM框架数据库&#xff1a; MySQL前端技术&#xff1a; 微信小程序, HTML…&#xff08;其它相关技术&#xff09; …

.BFS.

BFS &#xff08;Breadth-First Search&#xff09;是一种用于遍历或搜索树&#xff08;tree&#xff09;或图&#xff08;graph&#xff09;的算法。 这个算法从根&#xff08;或某个任意节点&#xff09;开始&#xff0c;并探索最近的邻居节点&#xff0c; 然后再探索那些节点…

adb的常见操作和命令

最近学习adb的时候&#xff0c;整理了一些adb的使用场景&#xff0c;如&#xff1a;adb与设备交互&#xff0c;adb的安装、卸载&#xff0c;adb命令启动&#xff0c;通过命令清除缓存&#xff0c;文件传输和日志操作。 adb的两大作用&#xff1a;在app测试的时候可以提供监控日…

如何高效测试防火墙的NAT64与ALG应用协议转换能力

在本文开始介绍如何去验证防火墙&#xff08;DUT&#xff09;支持NAT64 ALG应用协议转换能力之前&#xff0c;我们先要简单了解2个比较重要的知识点&#xff0c;即&#xff0c;NAT64和ALG这两个家伙到底是什么&#xff1f; 网络世界中的“翻译官” - NAT64技术 简而言之&…

【Linux安全】iptables防火墙(二)

目录 一.iptables规则的保存 1.保存规则 2.还原规则 3.保存为默认规则 二.SNAT的策略及应用 1.SNAT策略的典型应用环境 2.SNAT策略的原理 2.1.未进行SNAT转换后的情况 2.2.进行SNAT转换后的情况 3.SNAT策略的应用 3.1.前提条件 3.2.实现方法 三.DNAT策略及应用 1…

学习笔记——数据通信基础——数据通信网络(网络工程师)

网络工程师 网络工程&#xff0c;就是围绕着网络进行的一系列的活动&#xff0c;包括∶网络规划、设计、实施、调试、排错等。网络工程设计的知识领域很宽广&#xff0c;其中路由和交换是计算机网络的基本。 网络工程师∶是在网络工程领域&#xff0c;掌握专业的网络技术&…

Go 使用 RabbitMQ---------------之一

RabbitMQ 是一种消息代理。消息代理的主要目的是接收、存储并转发消息。在复杂的系统设计和微服务架构中,RabbitMQ 经常被用作中间件来处理和转发系统之间的消息,以确保数据的一致性和可靠性。正是因为提供了可靠的消息机制、跟踪机制和灵活的消息路由,常常被用于排队算法、…

SAP PP学习笔记 - 错误 CX_SLD_API_EXCEPTION - Job dump is not fully saved (too big)

我这个错误是跑完MRP&#xff0c;然后在MD04查看在库/所有量一览&#xff0c; 点计划手配&#xff08;Planned order 计划订单&#xff09;生成 制造指图&#xff08;Production order 生产订单&#xff09;&#xff0c; 到目前这几步都OK&#xff0c;然后在制造指图界面点保…

View->Bitmap缩放到自定义ViewGroup的任意区域

Bitmap缩放和平移 加载一张Bitmap可能为宽高相同的正方形&#xff0c;也可能为宽高不同的矩形缩放方向可以为中心缩放&#xff0c;左上角缩放&#xff0c;右上角缩放&#xff0c;左下角缩放&#xff0c;右下角缩放Bitmap中心缩放&#xff0c;包含了缩放和平移两个操作&#xf…

Golang | Leetcode Golang题解之第118题杨辉三角

题目&#xff1a; 题解&#xff1a; func generate(numRows int) [][]int {ans : make([][]int, numRows)for i : range ans {ans[i] make([]int, i1)ans[i][0] 1ans[i][i] 1for j : 1; j < i; j {ans[i][j] ans[i-1][j] ans[i-1][j-1]}}return ans }

NXP i.MX8系列平台开发讲解 - 3.12 Linux 之Audio子系统(一)

专栏文章目录传送门&#xff1a;返回专栏目录 目录 1. Audio 基础介绍 1.1 音频信号 1.2 音频的处理过程 1.3 音频硬件接口 1.3 音频专业术语解释 2. Linux Audio子系统介绍 3. Linux Audio子系统框架 Linux嵌入式系统中的音频子系统扮演着至关重要的角色&#xff0c;它涉…

Vue3+vite项目中使用mock模拟接口

安装依赖 分别安装vite-plugin-mock跟mockjs两个插件 npm install -D vite-plugin-mock mockjs vite.config.ts中添加配置&#xff0c;主要是红色标记的配置 注意此处如果配置出错可能是vite-plugin-mock依赖的版本有问题&#xff0c;重新安装一下依赖指定版本即可&#xf…

MX Component基础使用(多点位读取,多点位写入)

步骤&#xff0c;先连接PLC&#xff0c;然后在填入对应的点位 D10 然后去读取。 using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.Drawing;using System.Linq;using System.Text;using System.Threading.Tasks;us…

国产卫星星座,为什么一定要“走出去”?

今天这篇文章&#xff0c;我们来聊聊卫星和星座。 2024年行将过半&#xff0c;全球卫星通信产业的发展&#xff0c;又有了新的变化。 在卫星星座方面&#xff0c;各大企业的竞争博弈全面进入白热化阶段。卫星的发射速度在不断加快&#xff0c;而全球星座项目的数量和规模也在持…

nano机器人2:机械臂的视觉抓取

前言 参考链接: 【机械臂入门教程】机械臂视觉抓取从理论到实战 GRCNN 通过神经网络&#xff0c;先进行模型训练&#xff0c;在进行模型评估。 机械臂逆运动学求解 所有串联型6自由度机械臂均是可解的&#xff0c;但这种解通常只能通过数值解法得到&#xff0c;计算难度大&am…

Java | Leetcode Java题解之第118题杨辉三角

题目&#xff1a; 题解&#xff1a; class Solution {public List<List<Integer>> generate(int numRows) {List<List<Integer>> ret new ArrayList<List<Integer>>();for (int i 0; i < numRows; i) {List<Integer> row new…

Unity之如何使用Localization来实现文本+资源多语言

前言 使用Unity实现本地化&#xff08;Localization&#xff09;功能 在当今的游戏开发中&#xff0c;支持多语言已成为一项基本需求。Unity作为主流的游戏开发引擎&#xff0c;提供了强大的本地化工具&#xff0c;使开发者能够方便地为游戏添加多语言支持。本文将介绍如何在U…

JavaSE:StringBuilder和StringBuffer类

1、引言 在上一篇文章中&#xff0c;我们理解了字符串的常用方法&#xff0c;细心的同学大概已经发现&#xff0c;不管是将字符串中的字符转变为大写或小写&#xff0c;或是完成字符串的替换&#xff0c;又或是去除空白字符等等&#xff0c;只要涉及到字符串的修改&#xff0c…

js中金额进行千分以及toFixed()保留两位小数丢失精度的问题

1、金额进行千分 function commafy(num) { if ((num "").trim() "") { return ""; } if (isNaN(num)) { return ""; } num num ""; if (/^.*\..*$/.test(num)) { const pointIndex num.lastIndexOf("."); co…

[C][动态内存分配][柔性数组]详细讲解

目录 1.动态内存函数的介绍1.malloc2.free2.calloc4.realloc 2.常见的动态内存错误3.C/C程序的内存开辟4.柔性数组1.是什么&#xff1f;2.柔性数组的特点3.柔性数组的使用4.柔性数组的优势 1.动态内存函数的介绍 1.malloc 函数原型&#xff1a;void* malloc(size_t size)功能…