C++数据结构——AVL树

news2024/11/24 7:56:50

前言:本篇文章将紧随二叉搜索树的节奏,分享一个新的数据结构——AVL树。


目录

一.AVL树概念

二.AVL树插入规则

三.AVL树实现

1.基本框架

2.插入

3.旋转

1)左\右单旋

2)左右/右左双旋

4.遍历

5.求树高度

6.判断平衡

7.求树高度

总结


一.AVL树概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

但是当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

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

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


二.AVL树插入规则

由于AVL树的独特结构,我们给出以下的插入规则: 

1.按照搜索树规则插入。

2.更新插入节点的祖先节点的平衡因子:

        a.插入父亲的左边,父亲的平衡因子--

        b.插入父亲的右边,父亲的平衡因子++

        c.父亲的平衡因子 == 0,父亲所在的子树高度不变,不再往上更新,插入结束。

        d.父亲平衡因子 == 1 or -1,父亲所在的子树高度变了,往上更新,重复以上步骤。

        e.父亲平衡因子 == 2 or -2,父亲所在的子树已经不平衡了,需要旋转处理


三.AVL树实现

1.基本框架

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

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

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
private:
	Node* _root;
};

基本框架与平衡二叉树类似,区别在于AVL树的节点为键值对

同时我们还需增加平衡因子_bf和父节点_parent,方便我们进行调整。


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 (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 (kv.first < parent->_kv.first)
			parent->_left = cur;
		else
			parent->_right = cur;

		cur->_parent = parent;
		//更新平衡因子


		return true;
	}

基本的插入步骤与平衡二叉树一模一样,需要关注的就是插入的节点变为键值对

下面我们单独来看如何更新平衡因子

        while (parent)
		{
			if (cur == cur->_parent->_left)
				parent->_bf--;
			else
				parent->_bf++;

			if (parent->_bf == 0)
				//更新结束
				break;
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				//往上更新
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//出现问题,进行旋转
				break;
			}
			else
				assert(false);
		}

按照我们上边的规则其实很好写出上述代码,要注意循环条件为parent如果没有父亲,也就是到达了根节点,那就无法再进行调整。 

下面我们来重点关注,如何进行旋转


3.旋转

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

  1.  新节点插入较高左子树的左侧---左左:右单旋。
  2.  新节点插入较高右子树的右侧---右右:左单旋。
  3. 新节点插入较高左子树的右侧---左右:先左单旋再右单旋。
  4. 新节点插入较高右子树的左侧---右左:先右单旋再左单旋。

下面我们就一一来看这四种情况。


1)左\右单旋

先来看右单旋,可以抽象理解为,左子树过高,需要向右边旋转拉低。 

从上图能够看出,右单旋的步骤为:

  1. 让平衡因子为-2的节点成为它的左子节点的右子节点;
  2. 同时让该左子节点的右子节点成为平衡因子为-2的节点的左子节点。

同时我们需要关注的细节是:

  • 平衡因子为-2的节点是否为根节点。如果不是根节点则需要调整其父节点的指向。
  • 左子节点的右子节点是否为空。

通过这样的调整,就可以实现平衡,同时调整的两个关键节点的平衡因子均归0。 

下面来看代码:

	void RotateR(Node* parent)
	{
		//定义左子节点
		Node* subL = parent->_left;
		//定义左子节点的右子节点
		Node* subLR = subL->_right;
		//调整
		parent->_left = subLR;
		//判空
		if (subLR)
			subLR->_parent = parent;
		//调整
		subL->_right = parent;
		Node* ppNode = parent->_parent;
		parent->_parent = subL;
		if (parent == _root)//判断是否为根
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else//不是根节点,调整父节点指向
		{
			if (ppNode->_left == parent)
				ppNode->_left = subL;
			else
				ppNode->_right = subL;

			subL->_parent = ppNode;
		}
		//平衡因子归0
		parent->_bf = subL->_bf = 0;
	}

再来看左单旋: 

 左单旋则与右单旋完全相反,所以我们不做过多解释,直接给出代码:

	//左单旋
	void RotateL(Node* parent)
	{
		//定义右子节点
		Node* subR = parent->_right;
		//定义右子节点的左子节点
		Node* subRL = subR->_left;
		//调整
		parent->_right = subRL;
		//判空
		if (subRL)
			subRL->_parent = parent;
		//调整
		subR->_left = parent;
		Node* ppNode = parent->_parent;
		parent->_parent = subR;
		if (parent == _root)//判断是否为根
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else//不是根节点,调整父节点指向
		{
			if (ppNode->_left == parent)
				ppNode->_left = subR;
			else
				ppNode->_right = subR;

			subR->_parent = ppNode;
		}
		//平衡因子归0
		parent->_bf = subR->_bf = 0;
	}

2)左右/右左双旋

如果说树并不是子树的一条斜边独高,而是折线型的一颗子树高,此时单靠单旋是解决不了问题的,因此需要通过双旋来解决

上图所示为先左后右的折线型,所以我们需要进行左右双旋,步骤为:

  1. 先从折线的折点位置,即上图的30位置,进行左单旋,使树变为左边一条斜边独高的树。
  2. 在从折线起点位置进行右单旋。
  3. 更新平衡因子。

其中更新平衡因子也分为不同的情况,以上图为例:

  • 如果新节点插入位置为60的左,那么旋转后60为0,30为0,90为1。
  • 如果新节点插入位置为60的右,那么旋转后60为0,30为-1,90为0。
  • 如果新节点就是60,那么三者的平衡因子均为0。

下面上代码:

	//左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
		RotateL(parent->_left);
		RotateR(parent);
		if (bf == -1)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

 注意更新平衡因子是通过初始时折线末点的平衡因子判断的,所以要提前记录。


再来看右左双旋

 与左右双旋相反,右左双旋是先右后左的折线,所以其操作步骤与之相反:

  1. 先从折线的折点位置,即上图的90位置,进行右单旋,使树变为右边一条斜边独高的树。
  2. 在从折线起点位置进行左单旋。
  3. 更新平衡因子。

其中更新平衡因子也同样分为不同的情况,以上图为例:

  • 如果新节点插入位置为60的左,那么旋转后60为0,30为0,90为1。
  • 如果新节点插入位置为60的右,那么旋转后60为0,30为-1,90为0。
  • 如果新节点就是60,那么三者的平衡因子均为0。

代码如下:

	//右左双旋
	void RotateLR(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		RotateR(parent->_right);
		RotateL(parent);
		if (bf == -1)
		{
			subR->_bf = 1;
			subRL->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

根据父节点及其左右子节点的平衡因子,即可判断对应的旋转方式,下面补充插入步骤:

 

			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//出现问题,进行旋转
				//左单旋
				if (parent->_bf == -2 && parent->_left->_bf == -1)
				{
					RotateL(parent);
				}
				//右单旋
				else if (parent->_bf == 2 && parent->_right->_bf == 1)
				{
					RotateR(parent);
				}
				//左右单旋
				else if (parent->_bf == -2 && parent->_left->_bf == 1)
				{
					RotateLR(parent);
				}
				//右左单旋
				else
				{
					RotateRL(parent);
				}
				break;
			}

4.遍历

遍历操作与二叉搜索树类似,需要修改的是我们需要将键值对均打印出来:

	//遍历
	void InOrder()
	{
		inOrder(_root);
		cout << endl;
	}
	void inOrder(const Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		inOrder(root->_left);
		cout << root->_kv.first << ':' << root->_kv.second << " ";
		inOrder(root->_right);
	}

为了方便调用函数而无需传参,我们采用如上方式进行代码编写。 


5.求树高度

求树高度我们前边在讲解二叉树的时候已经分享过了,只需求出左右子树高度的最大值+1即可,通过递归计算:

	//求树高度
	int Height(const Node* root)
	{
		if (root == nullptr)
			return 0;
		return max(Height(root->_left), Height(root->_right)) + 1;
	}

6.判断平衡

判断树是否平衡,即判断两棵子树的高度差是否大于等于2

	//判断平衡
	bool IsBalance()
	{
		return isBalance(_root);
	}
	bool isBalance(const Node* root)
	{
		if (root == nullptr)
			return true;

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);
		if (abs(leftHeight - rightHeight) >= 2)
			return false;
		//检查平衡因子
		if (rightHeight - leftHeight != root->_bf)
			return false;

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

同时还需要通过递归来判断各个子树是否平衡


7.求树高度

求树的大小,通过递归即求左子树的大小+右子树的大小+根节点:

	//求树大小
	int Size()
	{
		return size(_root);
	}
	int size(const Node* root)
	{
		if (root == nullptr)
			return 0;
		return size(root->_left) + size(root->_right) + 1;
	}

总结

关于AVL树的基本内容就分享这么多,喜欢本篇文章的小伙伴记得一键三连,我们下期再见!

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

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

相关文章

WireShark对tcp通信数据的抓包

一、抓包准备工作 安装wireshark sudo apt update sudo apt install wireshark 运行 二、WireShark工具面板分析 上图中所显示的信息从上到下分布在 3 个面板中&#xff0c;每个面板包含的信息含义如下&#xff1a; Packet List 面板&#xff1a;显示 Wireshark 捕获到的所…

Vision Mamba:高效视觉表示学习双向状态空间模型,超越Vision Transformer!

DeepVisionary 每日深度学习前沿科技推送&顶会论文分享&#xff0c;与你一起了解前沿深度学习信息&#xff01; Vision Mamba: Efficient Visual Representation Learning with Bidirectional State Space Model 引言&#xff1a;探索视觉领域的新方向 在计算机视觉领域&…

【RSGIS数据资源】1980-2021年中国土地利用覆盖和变化数据集

文章目录 摘要1. 数据集概况2. 数据集组织形式2.1 1980-2015年中国森林覆盖数据集CFCD2.2 1980-2021年中国土地利用覆盖与变化数据集 3. 数据生产服务单位4. 引用 摘要 通过融合森林资源清查数据和20种遥感土地利用产品&#xff0c;重建生成了1980-2015年中国森林覆盖数据集&a…

JAVA大量数据导出excel

背景&#xff1a;因项目需要导出3万行&#xff0c;90列的数据到excel&#xff0c;使用传统的apache poi 直接导出&#xff0c;导致504连接超时无法导出。然后改造方法&#xff0c;异步导出。 一、准备一个导出类&#xff0c;属性有id&#xff0c;outputstrream,finleName,err,e…

五、Redis五种常用数据结构-SET

Redis的Set结构存储的数据和Java中的HashSet类似&#xff0c;都是无序且不重复的。其底层的数据结构有两种&#xff0c;一是当value为整数时&#xff0c;且数据量不大时采用intset来存储。其他情况使用dict字典存储。集合中最多存储232-1(40多亿)个数据。 1、常用命令 sadd k…

Vue中引入Element组件、路由router、Nginx打包部署

目录 1、Element-ui(饿了么ui) 演示&#xff1a; 怎么打开NPM脚本&#xff1f; Vue路由router Nginx打包部署Vue-Cli项目 1、Element-ui(饿了么ui) element-ui(饿了么ui)是一个非常好用且美观的组件库(插件库)&#xff0c;主要用于网站快速成型&#xff0c;由国产团队饿了么…

网工交换基础——VLAN Maping

一、定义 VLAN Maping通过修改报文携带的VLAN Tag来实现不同VLAN的相互映射。 二、应用场景 1、场景一&#xff1a;两个VLAN相同的二层用户网络通过骨干网络互联&#xff0c;为了实现用户之间的二层互通&#xff0c;以及二层协议&#xff08;例如MSTP等&#xff09;的统一部署…

如何内网穿透,远程访问内网设备

文章目录 0.前言1.准备工作2.内网穿透原理3.配置公网服务器的frp5.配置访问内网主机6.配置win10的远程桌面访问&#xff08;win11类似&#xff09;7.参考资料 0.前言 最近想研究一些新东西&#xff0c;公司的机器不敢乱搞&#xff0c;公司测试的服务器安装软件太多&#xff0c…

React - Input框绑定动态State和监听onChange事件,输入时失去焦点

React - Input框绑定动态State和监听onChange事件&#xff0c;输入时失去焦点 一. 案例复现二. 解决方案 一. 案例复现 案例代码如下&#xff1a; import React, { useState } from react; import { Table, Input } from antd; const Column Table.Column; const mockData …

CSP-j 计算机硬件

计算机系统 计算机系统由计算机硬件和软件两部分组成。硬件包括中央处理器、存储器和外部设备等&#xff1b;软件是计算机的运行程序和相应的文档。计算机系统具有接收和存储信息、按程序快速计算和判断并输出处理结果等功能。 主要技术指标 字长&#xff1a;字长是指CPU能够同…

anaconda虚拟环境pytorch安装

1.先创建conda的虚拟环境 conda create -n gputorch python3.102.激活刚刚创建好的虚拟环境 conda activate gputorch3.设置镜像源 这一步是后面安装pytorch相关包所需要的来源 pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple4.查看电脑的显卡…

【C++进阶】C++中的map和set

一、关联式容器 在初阶阶段&#xff0c;我们已经接触过STL 中的部分容器&#xff0c;比如&#xff1a; vector 、 list 、 deque&#xff0c; forward_list 等&#xff0c;这些容器统称为序列式容器&#xff0c;因为其底层为线性序列的数据结构&#xff0c;里面存储的是元素本…

PyQt5的布局管理

文章目录 1.垂直布局和水平布局垂直布局&#xff08;QVBoxLayout&#xff09;&#xff1a;水平布局&#xff08;QHBoxLayout&#xff09;&#xff1a; 2. 布局中的addStrech2.1 我们首先看只有一个Strech的情况&#xff0c;比较容易理解2.2 两个Strech2.3 多个Strech 3.栅格布局…

linux下使用jexus部署aspnet站点

1.运行环境 Centos 7 安装dos2unix工具 yum install dos2unix 安装jexus curl https://jexus.org/release/x64/install.sh|sudo sh2.网站部署 2.1. 将windows下的网站发布包Msc_qingdao_admin.zip上传到linux中&#xff0c; 然后解压后放入/var/www(没有则创建)目录下 r…

二叉树的遍历(前序 中序 后序)

一、前序遍历 顺序为&#xff1a; 根-->左子树---->右子树 先访问根节点&#xff0c;再递归进入根节点的左子树&#xff08;通过递归不断往下遍历&#xff09;&#xff0c;直到访问的节点没有左子树&#xff0c;此时递归进入其右子树&#xff08;通过递归进行相同操作&a…

简单问题汇总

一、vector和list 1.vector vector是可变大小数组的序列容器&#xff0c;拥有一段连续的内存空间&#xff0c;并且起始地址不变&#xff0c;因此能高效的进行随机存取&#xff0c;时间复杂度为o(1)&#xff1b;但因为内存空间是连续的&#xff0c;所以在进行插入和删除操作时…

机器学习算法应用——关联规则分析(4-4)

关联规则分析&#xff08;4-4&#xff09; 关联规则分析&#xff08;Association Rule Mining&#xff09;是一种基于频繁项集的分析方法&#xff0c;它以最常出现在一起的元素之间的关系作为分析对象&#xff0c;主要用于发掘大数据中隐藏的关联规则&#xff0c;是数据挖掘技术…

【吃透Java手写】4-Tomcat-简易版

【吃透Java手写】Tomcat-简易版-源码解析 1 准备工作1.1 引入依赖1.2 创建一个Tomcat的启动类 2 线程池技术回顾2.1 线程池的使用流程2.2 线程池的参数2.2.1 任务队列&#xff08;workQueue&#xff09;2.2.2 线程工厂&#xff08;threadFactory&#xff09;2.2.3 拒绝策略&…

求阶乘n!末尾0的个数溢出了怎么办

小林最近遇到一个问题&#xff1a;“对于任意给定的一个正整数n&#xff0c;统计其阶乘n&#xff01;的末尾中0的个数”&#xff0c;这个问题究竟该如何解决&#xff1f; 先用n5来解决这个问题。n的阶乘即n!5!5*4*3*2*1120&#xff0c;显然应该为2个数相乘等于10才能得到一个结…

记录minio的bug(Object name contains unsupported characters.)

场景是我将后端服务从121.xxx.xxx.xxx服务器上转移到了另一台服务器10.xxx.xxx.xxx 但图片都还在121.xxx.xxx.xxx服务器上&#xff0c;同样我10.xxx.xxx.xxx也安装了minio并且我的后端服务配置的minio地址也是10.xxx.xxx.xxx 此时有一个业务通过minio客户端获取图片&#xf…