C++ AVL树(旋转)

我们之前学习了搜索二叉树,我们知道普通的搜索二叉树会有特殊情况出现使得二叉树的两枝极其不平衡形成我们通俗说的歪脖子树:

这样的树一定会使得我们的增删查的效率变低;为了避免这种极端的情况出现,在1962年有两位伟大的俄罗斯数学家Adelson-VelskyLandis发明的;它们通过旋转使得我们的树的任意节点的左右子树高度差不超过2;使得搜索二叉树总会是一颗平衡的搜索二叉树;

我们怎么知道我们的左右子树的高度差呢?

AVL树的节点

我们这个时候就需要在搜索二叉树的基础上对我们的节点进行修改了,给我们的节点增加一个平衡因子_balance factor(_bf);

_bf平衡因子

介绍_bf:

_bf是如何计算的呢?创作者规定 _bf=右子树的深度-左子树的深度; 也就是说,我们每次我们插入一个新节点到我们当前节点的左边的时候_bf需要减一,当新节点插入到我们当前节点的右边的时候_bf加一;

template<class K,class V>
struct AVLTreeNode
{
	typedef AVLTreeNode<K,V> node;
	node* _left;
	node* _right;
	node* _parent;
	pair<K,V> _data;
	int _bf;//balance factor平衡因子
	AVLTreeNode(const pair<K, V>& data)
		:_left(nullptr)
		, _right(nullptr)
		,_parent(nullptr)
		,_data(data)
		,_bf(0)
	{}
};

可以看到,我们的AVL树的成员增加了_bf平衡因子和_parent父亲节点的指针;

有了描述每个节点的条件,要开始组织起来这些节点了,我们需要插入一个个节点;

插入节点

一开始插入节点是与搜索二叉树相同,我们需要先找到节点插入的位置,然后再将新增节点连接到我们的原树的叶子节点的末端,这个时候插入就完成了;

不同的是插入完成后,我们需要更新我们父亲节点的_bf平衡因子;采用上方所说的插入位置在父亲节点左侧时,父亲节点_bf-1,插入在父亲节点右侧时当前节点_bf+1;更新完成父亲节点后向上不断遍历修改父亲的父亲节点;而这个遍历我们又需要分情况遍历;

插入操作(和搜索二叉树中操作相同)
if (_root == nullptr)
{
	_root = new node(data);
	return true;
}
node* cur = _root;
node* parent = _root;
while (cur)
{
	if (data.first < cur->_data.first)
	{
		parent = cur;
		cur = cur->_left;
	}
	else if (data.first > cur->_data.first)
	{
		parent = cur;
		cur = cur->_right;
	}
	else
	{
		return false;
	}
}
cur = new node(data);
cur->_parent = parent;
if (data.first < parent->_data.first)
{
	parent->_left = cur;
}
else
{
	parent->_right = cur;
}
以上是普通的插入操作,下面需要进行平衡因子更新

更新_bf

情况1:父亲节点的_bf更新为1或-1

如果父节点的_bf为1或者1时就代表我们的父节点的左边或者右边新插入了节点,我们我们当前父节点为根的子树高度增加了,从而我们又会影响到父亲节点的父亲节点;所以我们需要继续所以向上遍历让父亲节点成为新插入的节点,更新父亲节点的父亲节点成为parent(父亲),更新新的父亲的_bf;

 情况2:父亲节点的_bf更新为0

 此时说明我们的父亲节点为根节点的树原本是不平衡的,但是由于我们插入的新节点补齐了这颗树;使得父亲节点的_bf变为了0,这个时候由于父亲节点为根的树高度没有变化;所以父亲节点上层的节点自然也不需要更新_bf所以这个时候可以直接退出遍历了;

情况3:父亲节点的_bf绝对值大于1

这个时候,我们需要进行旋转来降低父节点为根的树高度了

循环向上更新_bf
while (parent)
{
	
    if (cur == parent->_left)
	{
		parent->_bf--;
	}
	else
	{
		parent->_bf++;
	}
	if (parent->_bf == 1 || parent->_bf == -1)//-1or1时需要继续更新平衡因子
	{
		cur = parent;
		parent = parent->_parent;	
	}
	else if (parent->_bf == 0)//因子为0是将树补齐了不需要再向上更新了
	{
		break;
	}
	else if (parent->_bf == 2 || parent->_bf == -2)//2or-2时需要旋转减小树的高度
	{
		if (parent->_bf == -2 && parent->_left->_bf == -1)//左子树的左子树高右旋转
		{
			RotateR(parent);
		}
		else if (parent->_bf == 2 && parent->_right->_bf == 1)//右子树的右子树高左旋转
		{
			RotateL(parent);
		}
		else if (parent->_bf == -2 && parent->_left->_bf == 1)//左子树的右子树高,先右子树左旋再左子树右旋
		{
			RotateLR(parent);
		}
		else if (parent->_bf == 2 && parent->_right->_bf == -1)//右子树的左子树高,先左子树右旋再右子树左旋
		{
			RotateRL(parent);
		}
		else
		{
			assert(false);
		}
		break;
	}
}

 旋转

这个操作就是AVL最精华的部分了;我们知道是当树左右两边高度相差超高1的时候就需要旋转了;旋转主要是靠画图理解;

我们需要注意的是我们旋转的树是一般是子树(父节点为根时为整棵树);

而旋转又分为四种旋转情况:

为了方便描述我们将当前树的根节点同一叫根节点;

单旋

情况1:根节点_bf为-2根节点左节点_bf为-1时(右单旋)

当根节点_bf为-2时说明我们这棵树的左子树高度要比右子树高2个节点的高度,根左节点的_bf为-1又说明我们左子树的左子树比左子树的右子树高1个节点的高度,也就是如下图:

我们需要进行单旋,因为是左树高,所以需要进行右旋转,将根节点旋转为左节点的右节点

右单旋
void RotateR(node* parent)
{
	node* subl = parent->_left;
	node* sublr = subl->_right;

	parent->_left = sublr;
	if (sublr)
		sublr->_parent = parent;

	node* ppnode = parent->_parent;

	subl->_right = parent;
	parent->_parent = subl;

	if (ppnode == nullptr)
	{
		_root = subl;
		_root->_parent = nullptr;
	}
	else
	{
		subl->_parent = ppnode;
		if (ppnode->_left == parent)
		{
			ppnode->_left = subl;
		}
		else
		{
			ppnode->_right = subl;
		}
	}
	parent->_bf = subl->_bf = 0;
	return;
}

 情况2:根节点_bf为2根节点右节点_bf为1时(左单旋)

这种情况就是情况1的对立边,这次是右树的右子树高,我们需要进行左单旋;

左单旋
void RotateL(node* parent)
{
	node* subr = parent->_right;
	node* subrl = subr->_left;

	parent->_right = subrl;
	if (subrl)
		subrl->_parent = parent;

	node* ppnode = parent->_parent;
	subr->_left = parent;
	parent->_parent = subr;

	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;
}

 单旋之后我们的parent和左节点的_pf都为0;

双旋

情况3:根节点_pf为-2根节点的左节点_pf为1(左右双旋)

这种情况就是我们的根节点左树高,但左数中的右树高,我们需要进行双旋:

如何左右双旋如图:

 

值得注意的是双旋我们的_bf是需要更新的;我们需要通过sublr的_bf来判断更新值

 代码实现

void RotateLR(node* parent)
{
	node* subl = parent->_left;
	node* sublr = subl->_right;
	int flag = sublr->_bf;
	RotateL(subl);//左旋右子树(树中树)
	RotateR(parent);//右旋左子树
	//更新平衡因子
	if (flag == 1)//插入位置为树中树的右树
	{
		parent->_bf = 0;
		subl->_bf = -1;
		sublr->_bf = 0;
	}
	else if (flag == -1)
	{
		parent->_bf = 1;
		subl->_bf = 0;
		sublr->_bf = 0;
	}
	else if (flag == 0)
	{
		parent->_bf = 0;
		subl->_bf = 0;
		sublr->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

情况4:根节点_pf为2根节点的右节点_pf为-1(右左双旋)

此情况就是不同与单旋时的单一枝高,而是一枝中的另一枝高;

 

 同样的我们旋转之后更新我们的_bf;

代码实现

void RotateRL(node* parent)
{
	node* subr = parent->_right;
	node* subrl = subr->_left;
	int flag = subrl->_bf;
	RotateR(subr);//右旋左子树(树中树)
	RotateL(parent);//左旋右子树
	//更新平衡因子
	if (flag == 1)//插入位置为树中树的右树
	{
		parent->_bf = -1;
		subr->_bf = 0;
		subrl->_bf = 0;
	}
	else if (flag == -1)
	{
		parent->_bf = 0;
		subr->_bf = 1;
		subrl->_bf = 0;
	}
	else if (flag == 0)
	{
		parent->_bf = 0;
		subr->_bf = 0;
		subrl->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

 当上面的旋转和更新_bf都完成的时候,这棵树我们插入就一定是我们的AVL树了;

为了验证我们的树是否真的成为了AVL树我们需要通过检查算法来比较每个节点的_bf是否正确

void _print(node* root)
{
	if (root == nullptr)
		return;
	_print(root->_left);
	cout << root->_data.first << " ";
	_print(root->_right);
}

bool judgeBalance()
{
	return _judgeBalance(_root);
}

bool _judgeBalance(node*root)
{
	if (root == nullptr)
		return true;
	int hl = getDeep(root->_left);//hightLeft
	int hr = getDeep(root->_right);//hightRight
	int judge = hr - hl;
	if (judge != root->_bf)
	{
		cout << root->_data.first << "这个节点的_pf没处理好" << endl;
		return false;
	}

	return _judgeBalance(root->_left) && _judgeBalance(root->_right);
}

void testAVLTree1()
{
	AVLTree<int, int>t;
	srand(time(0));
	for (int i = 0; i < 100; i++)
	{
		int x = rand();
		t.insert(make_pair(x, x));
	}
	t.print();
	cout << t.judgeBalance() << endl;
}

此时我们的树就是一颗完全正确的AVL树 ;

我们只实现了AVL树的插入算法,删除算法还没有学习;

这是我的完整的AVL树代码:

C++代码/AVL树 · future/my_road - 码云 - 开源中国 (gitee.com)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

kubadm部署 kubernetes-1.29版本

一、集群节点准备 ip主机名称操作系统192.168.1.160master-1Centos-7.9192.168.1.161node-1Centos-7.9 二、安装前主机环境准备 &#xff08;所有主机都需要进行&#xff09; 1、配置主机名解析 echo "192.168.1.160 master-1" >> /etc/hosts echo "1…

C++符号清洗、Swift符号清洗, 编译还原

C 符号清洗&#xff08;编译还原&#xff09; C 由于函数重载的原因&#xff0c;针对每个函数符号&#xff0c;假如了name mangling的机制。导致堆栈适合机制阅读&#xff0c;因为每个函数符号都是独一无二的&#xff0c;但是这并非程序员易读的文字。 比如我们有这个符号cra…

又一AI工具开源!企业应该如何搭上这趟AI快车

大模型技术在近两年来飞速发展&#xff0c;企业对大模型的认知更加理性、务实。大模型本身不会直接产生价值&#xff0c;但在大模型基础架构之上开发出的AI应用&#xff0c;带来技术创新及业务增长&#xff0c;成为企业真正关心的问题。 基于大模型开发的又一个AI工具诞生&…

XenCenter 2024 导入虚拟机

导入虚拟机 虚拟机位置 导入到那一个服务器 导入虚拟机存放存储位置 虚拟机网卡配置 SR修复功能&#xff0c;看自己需求 虚拟机恢复确认最终配置 恢复好的虚拟机 虚拟机模板转换

源浩流体设备与您相约2024年第13届生物发酵展

参展企业介绍 温州源浩流体设备科技有限公司是一家集设计、开发、制造、销售、服务于一体的高科技企业&#xff0c;公司主要生产各种不锈钢阀门、管件、卫生级流体设备(卫生级换向阀,卫生级减压阀,卫生级罐底阀)等。现为温州市泵阀协会会员&#xff0c;ISO9000 2008版质量质量…

视频号视频下载小程序,让你随心保存你喜爱的视频!

今天给大家推荐一个非常实用的小程序&#xff0c;它就是专为下载视频号视频而设计的&#xff01; 微信视频号的兴起&#xff0c;让越来越多的优质、有趣的视频在平台上涌现&#xff0c;我们经常会遇到一些想要保存、回看的视频&#xff0c;但却无法轻易下载到手机或电脑中。这…

Linux基础篇:操作系统进程的基本概念与进程管理基础操作

Linux基础篇&#xff1a;操作系统进程的基本概念与进程管理基础操作 进程的定义&#xff1a; 进程是计算机系统中正在运行的程序的实例。 每个进程都有自己的内存空间、执行状态、资源和上下文。 进程是操作系统进行资源分配和调度的基本单位。 进程描述&#xff1a; 每个进…

LeetCode 63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish”&#xff09;。 现在考虑网格中有障碍物。那么从左上角到右下角…

华为CCE部署RabbitMQ中间件操作文档

1、创建有状态&#xff08;StatefulSet&#xff09;部署 中间件一般为有状态部署&#xff0c;有状态部署与无状态部署区别参考文档&#xff1a;K8S有无状态部署-CSDN博客 1.1、基本信息 注意&#xff1a; 应用名称命名规则&#xff1a;&#xff08;命名规则最好统一&#xff…

深入理解npm常用命令

npm&#xff08;Node Package Manager&#xff09;是 Node.js 的包管理工具&#xff0c;用于管理 Node.js 应用程序的依赖包。除了安装、更新和卸载依赖包外&#xff0c;npm 还提供了许多其他功能&#xff0c;如初始化项目、运行脚本、查看依赖树等。本文将详细介绍一些常用的 …

设计模式-行为型-中介者模式-Mediator

同事抽象类 public abstract class Colleague {private Mediator mediator;public abstract void play(String data); } 视频同事 public class AudioColleague extends Colleague {public void play(String data) {System.out.println("画外音是&#xff1a;" d…

GrayLog日志平台的基本使用-接入jumpserver

1、jumpserver3.8.0部署 Docker 环境准备 # 安装依赖包 yum install -y yum-utils device-mapper-persistent-data lvm2 # 添加源 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 替换Docker 安装源为清华大学镜像站 sed -i sh…

Linux-3 yum和vim

目录 本节目标&#xff1a; Linux 软件包管理器 yum 什么是软件包 1.yum是什么&#xff1f;软件包&#xff1f; 2.Linux(centos)的生态 3.yum的相关操作 我怎么知道我应该安装什么软件&#xff1f; 4.yum的本地配置 关于 rzsz 查看软件包 Linux编辑器-vim使用 1.v…

在电脑上怎么把视频做成二维码?视频生码的方法及步骤

在电脑上怎么把视频做成二维码呢&#xff1f;现在将视频存入二维码之后&#xff0c;将二维码分享或者打印出来&#xff0c;用这种方式来分享或者传递视频对比传统方式会更加的方便快捷。无需占用接收者的内存&#xff0c;手机扫码调取云端储存的视频&#xff0c;消耗视频流量来…

python 成员方法的区别是什么

Python的静态方法和类成员方法都可以被类或实例访问&#xff0c;两者概念不容易理清&#xff0c;但还是有区别的&#xff1a; 1&#xff09;静态方法无需传入self参数&#xff0c;类成员方法需传入代表本类的cls参数&#xff1b; 2&#xff09;从第1条&#xff0c;静态方法是…

redis集群搭建过程遇到的坑

在上一篇博文中搭建好了redis集群&#xff0c;但是仍然存在很多问题 上一篇&#xff1a;k8s下搭建redis集群 1、springboot服务连接 集群配置好了&#xff0c;spring服务应该怎么连接呢&#xff1f;单点和集群的连接配置写法是不一样的 单点 spring:redis:host: ${BTC_RED…

数控加工4轴初探

4轴加工之前一直觉得很神秘&#xff0c;最近画了些时间研究了一下&#xff0c;做过之后发现起始也不是特别复杂。 图中是两步&#xff0c;一步是粗开&#xff0c;已不是用指形铣刀精加工螺旋槽。

Unity Mesh 生成图形(二)

一、概述 Unity 的 Mesh 是用于表示三维物体的网格数据结构。它是由一系列顶点和三角形组成的网格&#xff0c;用于描述物体的形状和外观。 Mesh 是由顶点、三角形和其他相关信息组成的&#xff0c;它用于在 Unity 中创建和渲染三维对象。顶点是网格的基本构建单元&#xff0…

Xen Server 8 Install

Xen Sevrer 前言 XenServer&#xff08;以前称为 Citrix Hypervisor&#xff09;是业界领先的平台&#xff0c;实现了经济高效的桌面、服务器和云虚拟化基础结构。XenServer 支持任意规模或类型的组织整合计算资源&#xff0c;以及将计算资源转换为虚拟工作负载&#xff0c;从…

YOLOv7原创独家改进: 小目标 | CAMixing:卷积-注意融合模块和多尺度提取能力 | 2024年4月最新成果

💡💡💡本文独家改进:CAMixingBlock更好的提取全局上下文信息和局部特征,包括两个部分:卷积-注意融合模块和多尺度前馈网络; 💡💡💡红外小目标实现涨点,只有几个像素的小目标识别率提升明显 💡💡💡如何跟YOLOv8结合:1)放在backbone后增强对全局和局部特…