如何实现一棵AVL树

news2024/11/11 16:15:52

目录

1.什么是AVL树?

2.AVL树的实现

2.1AVL树结点的定义

2.2AVL树的插入

2.2.1插入的步骤

2.2.2插入情况分析

 2.2.3旋转操作的分析

2.3AVL树的查找

3.AVL树的验证

4.AVL树的性能分析


1.什么是AVL树?

AVL树其实就是一棵加了限制条件的二叉搜索树。由于二叉搜索树在插入元素为有序or接近有序的情况下,会退化成单支树,查找元素的时间复杂度变成了O(N),相当于在在顺序表中进行查找,那么树形结构的优势就完全体现不出来了, 伟大的计算机前辈觉不允许这种事情发生,于是两位俄罗斯的数学家就发明了AVL树;要想不让二叉搜索树退化成单支树,这两位数学家的想法是 保证每棵树及其子树的左右子树高度差的绝对值不超过1。当左右子树高度差的绝对值超过1,就需要调整了。

所以,AVL树具有以下性质:

  • 1.左右子树的高度差的绝对值不超过1;
  • 2.左右子树都是AVL树;
  • 3.空树也算是AVL树。

满足以上性质的树,能达到一种相对平衡的状态,所以AVL树也是一棵平衡二叉搜索树

为什么是左右高度差不超过1,不超过0不是更加平衡吗?有些条件下是无法做到左右子树高度差不超过0的,不超过1就是最好的状态。

2.AVL树的实现

数据结构的实现,一般只需实现其增、删、查、改;但AVL树也是一棵二叉搜索树,是不允许修改的,所以我们重点实现其插入操作。

2.1AVL树结点的定义

AVL树的结点可以像下面这样定义:

  • _left指向左孩子。
  • _right指向右孩子。
  • _parent指向父亲。
  • _bf是平衡因子,用来记录左右子树的高度差。(平衡因子并不是必须的,但是我们采用平衡因子的方式帮助我们控制树的结构)
  • _kv是存储的数据,用一个键值对来存储。

2.2AVL树的插入

2.2.1插入的步骤

AVL树的插入主要分为如下两步

  • 1.按二叉搜索树的规则插入;比当前结点大,就去右边插入,比当前节点小,就去左边插入。
  • 2.更新平衡因子;插入节点后,必然有树的平衡因子会变化,平衡因子 一旦不满足AVL树的性质,就需要调整了。(调整规则为在父结点的左边插入,父结点的平衡因子 减1,在父结点的右边插入,父结点的平衡因子 加1;如果更新后,父结点的父结点受到影响,还需要向上继续更新平衡因子

插入节点会影响哪些节点的平衡因子呢?

  • 如果在65的左边插入结点,65的平衡因子会发生变化,60的平衡因子也会发生变化,82的平衡因子也会发生变化,58的平衡因子也会发生变化。
  • 如果在83的左边插入结点,83的平衡因子会发生变化,82的平衡因子会发生变化
  • 所以插入结点会影响新增结点的部分祖先。

2.2.2插入情况分析

(注:下面的讲解用c代表新增结点,用p代表新增结点的父结点,pp代表父结点的父结点)

二叉树插入结点之后,树的形状有很多,但是大致可以划分成三种情况:p->_bf == 0,p->_bf == 1/-1,p->_bf == 2/-2。

  • 当插入结点后,p->_bf == 0,说明更新之前 p->_bf == 1/-1,在p的矮的那边插入了结点,p子树变得均衡了,p的高度不变,不会影响pp结点;更新结束。
  • 当插入结点后,p->_bf == 1/-1,说明更新之前 p->_bf == 0,在p的一边插入,p变得不均衡了,但是不违反规则,p的高度变了,可能会影响pp结点,需要继续往上更新。(更新之前p的平衡因子不可能是2/-2,因为AVL树的规则不允许出现这种情况,一旦出现平衡因子是2/-2的场景就需要调整)
  • 当插入结点后,p->_bf == 2/-2,说明p所在的子树违反了AVL树的规则,需要旋转处理;旋转处理 是将不平衡的AVL树变得平衡的一种手段,让p所在子树的高度回到了插入之前,不会对上层的平衡因子产生影响。

代码如下(旋转逻辑后面有详细代码示例):

    bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)  // 注意在空树中插入的情况
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)        // 往上更新到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 (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
            cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
            cur->_parent = parent;
		} // 判断新增的结点在父节点的哪一边插入

		while (parent)
		{
			if(cur == parent->left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			} //调节平衡因子
            
            // 判断要不要继续往上调整
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = cur->_parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 旋转处理,让不平衡的二叉搜索树变得平衡。
			}
		}

		return true;
	}

 2.2.3旋转操作的分析

我们之所以要进行旋转操作,是因为当前的子树出现了左右子树高度差为2/-2的情况,不满足AVL树的规则;所以我们需要降低当前树的高度,使当前这棵不平衡的子树变成平衡的子树。让p所在的子树的高度回到了插入之前,因此不会对上层子树的平衡因子产生影响。

二叉树的插入情况比较复杂,但是大致可以划分成 左单旋、右单旋、左右双旋、右左双旋。我们只需要分清出,什么情况下,使用哪一种旋转方式即可。

  • 左单旋:当p所在的子树的平衡因子为2,且c是p的右孩子,c所在的子树的平衡因子为1,此时就可以使用左单旋降低p所在子树的高度。如下图所示:
  • 右单旋:当p所在的子树的平衡因子为-2,且c是p的左孩子,c所在的子树的平衡因子为-1,此时就可以使用右单旋降低p所在子树的高度。如下图所示:
  • 左右双旋:当p所在的子树的平衡因子为-2,且c是p的左孩子,c所在的子树的平衡因子为1,此时就可以先对c所在的子树进行左单旋,再对p所在的子树进行右单旋。如下图所示:
  • 右左双旋:当p所在的子树的平衡因子为2,且c是p的右孩子,c所在的子树的平衡因子为-1,此时就可以先对c所在的子树进行右单旋,再对p所在的子树进行左单旋。如下图所示:

旋转之后,平衡因子是怎么变化的呢?读者可以自己画画图,就可以总结出规律了;

  • 左单旋和右单旋都是让原来的p和c所在的子树的平衡因子变为0。
  • 左右双旋之后,平衡因子的变化比较复杂,但是可以根据原来c的右孩子的平衡因子的大小来划分情况,如下图所示:

  • 右左双旋之后,平衡因子的变化可以根据原来c的左孩子的平衡因子的大小来划分情况,如下图所示:

2.3AVL树的查找

在AVL树中查找一个结点,就相当于在二叉搜索树中查找一个结点,找到了就返回该结点的地址,没找到就返回空指针,代码如下:

    Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return NULL;
	}

3.AVL树的验证

当我们实现了一棵简单的AVL树之后,该如何检测该AVL树实现的是否正确呢?

验证是否是合格的AVL树分为两步 1.验证其是否是二叉搜索树,可以通过中序遍历,看其数据是否是升序判断。2.验证其是否符合AVL树的性质,我们不能通过检测所有子树的平衡因子来判断,因为,我们不敢百分之百保证平衡因子的更新是正确的,只能求出每个子树的左右高度差来判断。代码如下:

    bool _IsBalance(Node* root, int& height)
	{
		if (root == nullptr)
		{
			height = 0;
			return true;
		}

		int leftHeight = 0, rightHeight = 0;
		if (!_IsBalance(root->_left, leftHeight) 
			|| !_IsBalance(root->_right, rightHeight))
		{
			return false;
		}

		if (abs(rightHeight - leftHeight) >= 2)
		{
			cout <<root->_kv.first<<"不平衡" << endl;
			return false;
		}

		if (rightHeight - leftHeight != root->_bf)
		{
			cout << root->_kv.first <<"平衡因子异常" << endl;
			return false;
		}

		height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;

		return true;
	}

	bool IsBalance()
	{
		int height = 0;
		return _IsBalance(_root, height);
	}

4.AVL树的性能分析

AVL树是对二叉搜索树的优化升级,很好的克服了二叉搜索树在插入数据的顺序为有序 or 接近有序时退化成单支树的问题,保证了查询的时间复杂度为O(log_N),但是其代价就是通过更多的旋转来控制;如果要对AVL树做一些结构上的修改操作,修改操作很有可能会改变子树的平衡因子,从而引发旋转,如果是删除操作的话,可能要旋转到根的位置,效率低下。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

基于AVL树的缺点,有大佬发明了另一种平衡二叉搜索树 —— AVL树,我们后面讲解。    

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

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

相关文章

day38.动态规划+MySql数据库复习

844.比较含退格的字符串 给定 s 和 t 两个字符串&#xff0c;当它们分别被输入到空白的文本编辑器后&#xff0c;如果两者相等&#xff0c;返回 true 。# 代表退格字符。 注意&#xff1a;如果对空文本输入退格字符&#xff0c;文本继续为空 思路:定义两个栈&#xff0c;将字符…

集合及数据结构第九节————树和二叉树

系列文章目录 集合及数据结构第九节————树和二叉树 树和二叉树 树型结构的概念树的概念树的表示形式&#xff08;了解&#xff09;树的应用二叉树的概念两种特殊的二叉树二叉树的性质二叉树的性质练习二叉树的存储二叉树的遍历二叉树的基本操作二叉树相关练习题 文章目录…

flutter 中 ssl 双向证书校验

SSL 证书&#xff1a; 在处理 https 请求的时候&#xff0c;通常可以使用 中间人攻击的方式 获取 https 请求以及响应参数。应为通常我们是 SSL 单向认证&#xff0c;服务器并没有验证我们的客户端的证书。为了防止这种中间人攻击的情况。我么可以通过 ssl 双向认证的方式。即…

Leetcode JAVA刷刷站(91)解码方法

一、题目概述 二、思路方向 这个问题是一个典型的动态规划问题&#xff0c;其中我们可以使用一个数组来存储到达每个位置时的解码方法的总数。 我们定义一个数组 dp&#xff0c;其中 dp[i] 表示字符串 s 的前 i 个字符&#xff08;从索引 0 到 i-1&#xff09;的解码方法总数。…

企业数字化转型管控平台探索 ---基于流程的企业经络管理框架DEM

篇幅有限&#xff0c;获取完整内容、更多感兴趣的内容 见下图

OpenCV几何图像变换(8)调整图像大小的函数resize()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 resize 函数调整图像 src 的大小&#xff0c;使其缩小或放大至指定的大小。需要注意的是&#xff0c;初始的 dst 类型或大小不被考虑。相反&…

解决Jasper Studio报表工具中预览正常显示,但部署到服务器上面无法正常显示的问题

目录 1.1、错误描述 1.2、解决方案 1.1、错误描述 之前有遇到过一个Jasper Studio报表开发相关的问题&#xff0c;这里记录一下&#xff0c;方便其他小伙伴可以快速解决问题。问题是这样的&#xff1a;当我在Jasper Studio报表工具里面设计好样式之后&#xff0c;预览报表发…

[论文阅读] mobile aloha实验部分

DP:[1] CHI C, FENG S, DU Y, et al. Diffusion Policy: Visuomotor Policy Learning via Action Diffusion[J]. 2023. Diffusion Policy: Visuomotor Policy Learning via Action Diffusion精读笔记&#xff08;一&#xff09;-CSDN博客 VINN:[1] PARI J, SHAFIULLAH N, ARU…

视频达人的秘密武器:全能型剪辑软件深度剖析

剪辑视频&#xff0c;作为视频创作过程中的关键环节&#xff0c;其重要性不言而喻。无论是专业影视制作团队&#xff0c;还是热衷于Vlog创作的个人&#xff0c;都离不开一款强大且易用的视频剪辑工具。今天&#xff0c;就让我们一起踏上一场探索之旅&#xff0c;对市面上的视频…

java基础 之 关键字static

文章目录 前言1、特征2、修饰变量3、修饰方法4、修饰代码块优缺点应用场景代码理解 前言 本文主要是从类与对象的方向来讲&#xff0c;所以在文章开始前&#xff0c;我们先理解一下类和对象 类是一个模板&#xff0c;对象是一个实例。 如【手机】是一个类&#xff08;一个模板…

MySQL系统性的学习--基础

学习资料是黑马的mysql课程 Mysql概述 相关概念 数据模型 关系型数据库 数据模型 SQL SQL通用语法 SQL分类 DDL 数据库操作 表操作 查询 创建 数据类型 修改/删除 DML 添加数据INSERT 修改数据UPDATE 删除数据DELETE DQL 基础查询 条件查询 聚合函数 分组查询 排序查询 分…

Otterctf 2018 内存取证 (复现)

题目地址: https://otterctf.com/challenges 1 - What the password? 描述:you got a sample of ricks PCs memory. can you get his user password? 首先查看一下镜像的信息 python2 vol.py -f /home/kali/Desktop/OtterCTF.vmem imageinfo 题目描述需要获取密码, 使用mi…

el-form中使用v-model和prop实现动态校验

如何在Vue的el-form中使用v-model和prop实现动态校验&#xff0c;包括多个变量控制校验、数组循环校验和字段级条件显示。通过实例演示了如何配合rules和自定义验证函数来确保表单的完整性和有效性。 公式&#xff1a; 动态校验项的v-model的绑定值 el-form的属性 :model的值 …

PCSE不同播种时间的对比

目录 简介对比图源代码简介 设置为2022年10月15日播种,然后每隔5天往后播种一次,然后探究播种时间对于作物各个长势的影响 对比图 源代码 import sys, os import matplotlib from matplotlib import style matplotlib.style.use("seaborn-whitegrid") import ma…

ST 表算法

ST 表 ST 表&#xff0c;主要思想是空间换时间&#xff0c;用于解决可重复贡献问题和 RMQ 问题。 可重复贡献问题 指某个运算 o p op op&#xff0c;有 x o p x x x\ op\ x\ \ x x op x x 。例如 m a x ( x , x ) x m i n ( x , x ) x g c d ( x , x ) x max(x,x)x\…

Linux基础环境开发工具gcc/g++ make/Makefile git

1.Linux编译器-gcc/g使用 1. 预处理&#xff08;进行宏替换) 预处理功能主要包括宏定义,文件包含,条件编译,去注释等。 预处理指令是以#号开头的代码行。 实例: gcc –E hello.c –o hello.i 选项“-E”,该选项的作用是让 gcc 在预处理结束后停止编译过程。 选项“-o”是指目标…

UE基础 —— Components

目录 Component Instancing Instanced Static Mesh Component Instanced Static Mesh Differences of an ISM and a Static Mesh Component Hierarchical Instanced Static Mesh Instancing Systems Working with ISMs Prefabrication Custom Data Creating and Edit…

吴恩达机器学习课后作业-04神经网络

神经网络 对y进行独立热编码处理&#xff08;one-hot处理&#xff09;序列化权重参数前向传播代价函数反向传播神经网络优化可视化隐藏层 对y进行独立热编码处理&#xff08;one-hot处理&#xff09; def one_hot_encoder(raw_y):result[]for i in raw_y:#1-10y_tempnp.zeros(1…

网络编程之初识

目录 ​前言 发展史 网络互连 局域网(LAN) 广域网(WAN) 网络通信基础 IP地址 特殊IP地址 端口号 网络协议 协议的作用 五元组 协议分层 含义 OSI七层模型 TCP/IP五层协议 网络设备所在分层 封装和分用 发送方 接收方 前言 在这个科技发达的时代&#xff0…

初识C语言指针(3)

目录 1. 数组名的理解 2. 使⽤指针访问数组 3. ⼀维数组传参的本质 4. 冒泡排序 5. 二级指针 6. 指针数组 7. 指针数组模拟⼆维数组 结语 1. 数组名的理解 对于数组名想必大家并不陌生&#xff0c;数组名就是该数组首元素的地址&#xff0c;设想有一个arr 数组。我们…