二叉树改良版——AVL树

news2024/11/26 10:36:51

为什么说是“改良”,其实标题的二叉树指的是搜索二叉树,它虽然可以缩短查找的效率,但如果数据已经有序或接近有序的话二叉树就会退化成单支树,这样查找元素的话反而会效率低下。因此,为了解决这个问题,AVL树就得以出现,它的特征是:当向二叉搜索树插入新结点后,如果能保证每个结点的左右子树高度差的绝对值不超过1(调整后的),即可降低树的高度,进而减少搜索频率,提高效率。

一、AVL树简介

一棵AVL树具有以下性质:1.它的左右子树都是AVL树 2.左右子树高度之差(简称平衡因子)的绝对值不超过1。但后续我们为了方便研究,平衡因子通常是右子树高度-左子树高度。

那么如果我要插入一个数据,那会影响哪些结点的平衡因子呢?答案是其部分祖先,为什是部分呢?插入后,平衡因子会更新,若插入右子树,平衡因子++,插入在左子树,平衡因子--(其父节点的平衡因子变化规律),但是再向上找其祖先的平衡因子就不一定会变化,若父节点只有右子树,此时将数据插入左子树,那么其祖先的平衡因子不会变化。(因为其子树的高度没有变化)所以我们说是影响部分祖先。

我们来列举以下情况来分析,什么情况下需要向上更新祖先平衡因子什么时候又不需要。

假设我们现在插入了一个新结点后,其父节点parent平衡因子为0,1/-1,2/-2

第一种情况,说明我插入的位置一定是父节点左右子树中缺少的那个位置(若parent有左(右)那么一定插入的是右(左))此时parent的子树高度没变化,故不需要找祖先。

第二种情况,说明插入前的parent一定是0(不可能是2/-2,因为这样的情况就不满足AVL树的定义了),故插入后parent会多了左(右)子树,高度也会变化,需要找祖先。

第三种情况,说明parent更新前是1/-1,若其有左(右)子树的话会继续插入到其左(右)子树,高度也会增加,也需要找祖先。但此时已经违反了AVL的规则了,需要进行旋转,我们稍后会用代码实现。

我们先进行其结点的封装,因为AVL树的本质也是二叉搜索树,所以我们用之前的模板即可,这里还需要加工一下,用一个新的容器——map,map的本质虽然也是二叉搜索树,但它每个结点的存放是key-value类型(他们被一个pair的类封装又重命名为first和second)(类似于查字典的功能,一一对应),与其相似的还有一个容器set,它的底层就是我们熟悉的二叉搜索树了(有关set和map的封装作者会在下篇或下下篇总结)

接下来,我们看看平衡因子的更新

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=parent;
		parent = parent->_parent;//向上找祖先
	}
	else if (parent->_bf == 2 || parent->_bf == -2)
	{
		//不平衡了,需要旋转
	}
	else
	{
		assert(false);
	}
}

注意:这段代码是放在结点插入的函数中且是在插入操作之后进行的,因为平衡因子改变的原因就是进行了插入操作。

二、AVL树的旋转

AVL树的旋转分为四类

1.右右插入——左单旋

我们来看下面这个情景

我们给几个数据命名一下:30——parent,60——subR,b——subRL,那么左单旋的思路就是:subRL变成parent的右,parent变成subR的左,subR变成parent,但实际代码实现时需要考虑很多情况,比如subR为空时情况又是怎样的。

代码如下:

void rotatel(Node*parent)
{
  Node*subR=parent->_right;
  Node*subRL=subR->_left;
   parent->_right=subRL;
    if(subRL)//如果不为空才能调整其父节点
     subRL->_parent=parent;

  Node*Parentparent=parent->_parent;//记录根结点的父节点,因为不确定调整的是整棵树还是一部分子树
    subR->left=parent;
  parent->_parent=subR;//每次调整后别忘调整其父节点
 if(Parentparent==nullptr)//说明调整的是整棵树,那么新的根就是subR
{
_root=subR;
subR->_parent=nullptr;//别忘置空,因为此时subR父节点还是parent
}
else//调整的是子树
{
   if(parent==Parentparent->_left)
   {
     Parentparent->_left=subR;
   }
   else
   {
     Parentparent->_right=subR;
   }
   subR->_parent=Parentparent;
}
parent->_bf=subR->_bf=0;
}

 
  

2.左左插入——右单旋 (左单旋的镜像)

代码与左单选是类似的,在此我们直接呈现代码

void rotater(Node*parent)
{
 Node*subL=parent->_left;
 Node*subLR=subL->_right;
  parent->_left=subLR;
   if(subLR)
     subLR->_parent=parent;

  Node*Parentparent=parent->_parent;
  subL->_right=parent;
  parent->_parent=subL;
 if(Parentparent==nullptr)
{
  _root=subL;
   subL->_parent=nullptr;
}
else
  {
   if(parent==Parentparent->left)
    {
     Parentparent->_left=subL;
    }
    else
    {
      Parentparent->_right=subL;
    }
     subL->_parent=Parentparent;
  }
parent->_bf=subL->_bf=0;
}

有了两个旋转的函数,我们就可以初步完善之前的插入代码了,我们选择摘取前面的一部分填写

else if (parent->_bf == 2 || parent->_bf == -2)
	{
		//不平衡了,需要旋转
        //利用平衡因子判断旋转方式,默认右-左
       if(parent->_bf == 2&&cur->_bf==1) //左旋
          rotatel(parent);
       else if(parent->_bf == -2&&cur->_bf==-1)//右旋
          rotater(parent);	
    }

接下来我们就要介绍一些稍微复杂的情况了

3.右左双旋——新节点插入较高右子树的左侧

我们假设以下情景:(注:下面说的所有 ab高度相同,cd高度相同,但ab比cd高一层)

如果abc均为空,d为70,此时如果把数据插入到d的左或右都满足上面的左单旋实现平衡,但如果我插入的位置是b或者c呢?如果通过左单旋进行旋转,发现其并不能达到平衡的效果。我们引进一个新的旋转方式——右左双旋。

我们先看最简单一只情况,假设此时abcd均为空,在90左插入60.(按前面的道理我们知道,无论是左旋还是右旋都无法平衡)接下来的操作:60变成30的右边,90变成60的右边,此时我们发现进行这种操作后情况符合左单旋的情况了,所以在此基础上再来一次左单旋即可(如果在左边同理),第一步的操作我们也叫右单旋(右左双旋中的右单旋)。

上面最简单的情况是不足以说明右单旋的原理的,看下面的情况,假设abcd均存在(bc是新插入的数据,如果插入在d的左右就符合左单旋的情形了,故插在60左右),右单旋的原理就是:60变成30的右,90变成60的右,c变成90的左,其他不变。至此,右单旋结束

下一步,左单旋,结果如下:

其实我们发现,在进行右左双旋之后,如果以60为中心,30为左,90为右,那么该操作的结果就是把中间的左给了左的右,中间的右给了右的左。

幸运的是,这里我们可以复用前面的函数,套一层就好了,代码如下:

void rotaterl(Node*parent)
{
    rotater(parent->_right);//右旋以90为中心
    rotatel(parent);
}

但这个双旋的难处在于其平衡因子的调整,我们假设现在树的情形是,abcd均存在且还要再bc间选择一个地方插入。经分析,插在b和c对于30和90来说两次的平衡因子是不一样的,所以我们需要先知道其插入的位置才能确定平衡因子。很明显,看60那个位置的平衡因子就可以了。

但,如果插入的数据是60呢(也就是abcd均不存在),看结果我们知道最后的平衡因子都是0,所以这个情况比较简单。因此我们还要分情况讨论。

插入后,如果subRL(60)的平衡因子是1说明插在了c下,-1就是b下,0就是60本身是插入的数据。

上面的代码只是实现了结点关系的转换并没有对平衡因子进行修改,所以我们需要完善一下代码。

void rotaterl(Node*parent)
{
    Node*subR=parent->_right;
    Node*subRL=subR->_left;
    int bf=subRL->_bf;
 //在旋转之前进行变量的存储以便于对后续平衡因子的修改
    rotater(parent->_right);//右旋以90为中心
    rotatel(parent);
    if(bf==0)
    {
      subR->_bf=0;
      subRL->_bf=0;
      parent->_bf=0;
    }
    else if(bf==1)//插在了c侧
    {
      parent->_bf=-1;
      subR->_bf=0;
      subRL->_bf=0;
    }
    else if(bf==-1)//插在了b侧
    {
     parent->_bf=0;
     subR->_bf=1;
     subRL->_bf=0;
    }
    else 
    {
     assert(false);
    }
}

4.左右双旋—新节点插入较高左子树的右侧

与右左双旋类似,这边直接上代码加旋转流程图

void rotatelr(Node*parent)
{
  Node*subL=parent->_left;
  Node*subLR=subL->_right;
  int bf=subLR->_bf;
    rotatel(parent->left);
    rotater(parent);
  if(bf==0)
  { 
   parent->_bf=0;
   subL->_bf=0;
   subLR->_bf=0;
  }
  else if(bf==-1)
  {
   parent->_bf=1;
   subL->_bf=0;
   subLR->_bf=0;
  }
  else if(bf==1)
  {
   parent->_bf=0;
   subL->_bf=-1;
   subLR->_bf=0;
  }
  else
  {
   assert(false);
  }
}  

三、AVL树的验证

上面我们完善了AVL树,但其外表本质还是二叉搜索树,如果按中序遍历输出结果上和正常的二叉搜索树没有区别,无法验证其内部的高度是否平衡。因此我们需要验证其平衡性进而区分出他们的区别。很明显,区分二者最明显的方法就是利用平衡因子。

int _height(Node*root)
{ 
   if(root==nullptr)
       return 0;
   int leftheight=_height(root->_left);
   int rightheight=_height(root->_right);

return leftheight>rightheight ? lefthright+1 : rightheight+1;
}

bool _isbalancetree(Node*root)
{
    if(root==nullptr)//空树也是AVL树
      return true;
    int leftheight=_height(root->_left);//利用计算高度函数获得当前高度
    int rightheight=_height(root->_right);
    int diff=rightheight-leftheight;//高度差,即根结点的平衡因子
// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
 // Root平衡因子的绝对值超过1,则一定不是AVL树
    if(abs(diff)>=2||diff!=root->_bf)//abs是绝对值函数,解决了高度差正负的问题
       return false;
//如果左右子树都是AVL树,该树一定是AVL树
    return _isbalancetree(root->_left)&&_isbalancetree(root->_right);
}

除此之外,我们也可以写一个函数记录AVL树中的结点个数,还是递归思想,这里就不展示了。(需要注意的是,由于上面的函数均需要传结点,但是private成员不可访问,所以我们要在外套一层函数传参才能使用)

至于AVL树的删除作者在此就不多讲解了,思路虽然和插入操作的原理类似,但其情况分析其实要比插入复杂的多,涉及到按搜索树的规则删除,更新平衡因子,其异常后还要旋转处理。

以上就是AVL树的一些基本内容了,其中比较重要的是插入以及旋转的原理和平衡因子的调整,建议小伙伴们反复关顾哦,请留下你宝贵的三连再走,这对我更新的动力真的很大(doge)。

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

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

相关文章

zynq 添加lwip库

在自己的项目属性中. 就是在上一行的下面加了一行配置. 多了个 -llwip4 -Wl,--start-group,-lxil,-llwip4,-lgcc,-lc,--end-group

第十四届单片机嵌入式蓝桥杯

一、CubeMx配置 (1)LED配置 (1)LED灯里面用到了SN74HC573ADWR锁存器,这个锁存器有一个LE引脚,这个是我们芯片的锁存引脚(使能引脚),由PD2这个端口来控制的 (2&#xff…

Qt初识_通过代码创建hello world

个人主页:C忠实粉丝 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C忠实粉丝 原创 Qt初识_通过代码创建hello world 收录于专栏【Qt开发】 本专栏旨在分享学习Qt的一点学习笔记,欢迎大家在评论区交流讨论💌 目录 1.通过按…

魔珐出席INSIGHT金融洞察力峰会,共探AI内容生成新范式

2024年9月27日,2024INSIGHT金融洞察力在北京举行,来自银行、保险、期货、证券、基金等行业的业界翘楚,共商行业热点议题,为金融行业增进互信、扩大合作搭建闭门平台,贡献价值与力量。 魔珐科技AIGC业务负责人杜子航&a…

XUbuntu安装OpenSSH远程连接服务器

目录 打开终端。更新你的包索引安装OpenSSH服务器。在终端中输入以下命令:安装完成后,OpenSSH服务器会自动启动。查看主机 IP测试连接打开 cmd 终端SSH 连接虚拟机确认连接输入连接密码发现问题修改用户,尝试连接 打开终端。 更新你的包索引 …

候机时间计算(数学小题目,练习时间字符串“解析”)

时间字符串的简单处理,可自行解析也可以调库。 (笔记模板由python脚本于2024年10月10日 18:06:42创建,本篇笔记适合有基本编程逻辑的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网:https://www.python.org/ Free:大咖免费“…

MinIO 学习订阅服务

MinIO 的入门非常简单 — 只需几个简单的命令和一个 100 MB 的小二进制文件,您就可以立即启动并运行一个功能性开发环境。但是,为了在生产规模上利用 MinIO 的全部功能,我们鼓励专业人士更多地了解 MinIO 的广泛功能。我们推出了 MinIO 学习订…

Spring Boot课程问答:技术难题专家解答

摘要 随着信息互联网信息的飞速发展,无纸化作业变成了一种趋势,针对这个问题开发一个专门适应师生交流形式的网站。本文介绍了课程答疑系统的开发全过程。通过分析企业对于课程答疑系统的需求,创建了一个计算机管理课程答疑系统的方案。文章介…

企业远控私有化部署解决方案-内信互联

内信互联(DoLink),是点量软件新推出的企业私有化远程控制系统解决方案。很多朋友对这个产品还不是很了解,今天点量小编就对其基础功能做一些详细说明,如果您想快速拥有自己的企业私有远程控制系统,欢迎联系…

xavier 在tensorflow pytorch中的应用,正太分布和均匀分布的计算公式不一样

Xavier初始化,也被称为Glorot初始化,是一种用于深度神经网络的权重初始化方法。这种方法是由Xavier Glorot和Yoshua Bengio在2010年的论文《Understanding the difficulty of training deep feedforward neural networks》中提出的。Xavier初始化的主要目…

bpmn-js 元素与布局渲染

BPMN-JS 是基于 BPMN 2.0来定义元素关联关系,并通过Diagram-js库来实现web可视化的显示和编辑工作。Diagram-js 也是由BPMN.IO组织开发的一个专门用于业务流程建模符号(BPMN)的可视化开源 JavaScript 库。 元素(Elements) BPMN 2.0(Business Process Model and Notation…

Windows docker 部署MiGPT+ 本地Ollama

1. 下载 MiGPT https://github.com/idootop/mi-gpt https://github.com/idootop/mi-gpt/releases/tag/v4.2.0 2. 运行 Ollama qwen模型 3.配置Mi GPT .env .migpt.js 运行docker 运行 需要上网 docker run -d --env-file D:\LLM\mi-gpt-4.2.0\.env -v D:\LLM\mi-gpt-4.2.0…

【读书笔记·VLSI电路设计方法解密】问题12:制造MOSFET晶体管的主要工艺步骤是什么

VLSI芯片是在半导体材料上制造的,这种材料的导电性介于绝缘体和导体之间。通过一种称为掺杂的工艺引入杂质,可以改变半导体的电气特性。能够在半导体材料的细小且定义明确的区域内控制导电性,促使了半导体器件的发展。结合更简单的无源元件(电阻、电容和电感),这些器件被…

3D汽车动画:技术、应用与行业影响

3D汽车动画,凭借其逼真的可视化效果和动态功能,已成为汽车行业展示创新和技术实力的重要工具。它不仅能够细致地呈现产品功能,还能模拟复杂的驾驶场景,帮助客户全面了解汽车的性能和设计。3D汽车动画的应用不仅加强了汽车设计展示…

给定任意非空有向图 G,输出 G 中所有 K 顶点的算法,并返回 K 顶点的个数。

已知优先图 G 采用邻接矩阵存储是,其定义如下 typedef struct { // 图的定义 int numVertices, numEdges; // 图中实际的顶点数和边数 char VerticesList[MAXV]; // 顶点表,MAXV为已定义常量 int Edge[MAXV]…

QD1-P9 HTML 超链接标签(a)上篇

本节学习&#xff1a;HTML 超链接标签&#xff0c;也就是 a 标签。 在前端开发中&#xff0c;<a>​ 标签是超链接&#xff08;anchor&#xff09;标签&#xff0c;用于创建指向其他网页、文件、位置等的链接。 本节视频 www.bilibili.com/video/BV1n64y1U7oj?p9 简单示…

laravel DCAT 中如何修改面包屑导航栏内容

dcat中修改面包屑 一、背景二、找到设置的方法三、修改面包屑 一、背景 DCAT的页面还是非常干净的&#xff0c;当设置语言格式为zh_CN以后&#xff0c;发现面包屑导航还有英文&#xff0c;如下图所示&#xff1a; 二、找到设置的方法 根据dcat文档介绍&#xff0c;页面分为…

IPv 4

IP协议 网络层主要由IP&#xff08;网际协议&#xff09;和ICMP&#xff08;控制报文协议&#xff09;构成&#xff0c;对应OSI中的网络层&#xff0c;网络层以实现逻辑层面点对点通信为目的。目前应用最广泛的IP协议为IPv4 基本概念给出 主机&#xff1a;配有IP地址但不具有路…

Visual Studio Code基础:使用debugpy调试python程序

相关阅读 VS codehttps://blog.csdn.net/weixin_45791458/category_12658212.html?spm1001.2014.3001.5482 一、安装调试器插件 在VS code中可以很轻松地调试Python程序&#xff0c;首先需要安装Python调试器插件&#xff0c;如图1所示。 图1 安装调试器插件 Python Debugge…

在当前网络环境中查看所有IPv4与Mac地址的方法

在powershell界面中&#xff1a; # 获取并显示所有网络接口的MAC地址和IPv4地址 Get-NetAdapter | Select-Object -Property Name, MacAddress, Status Get-NetAdapter | Get-NetIPAddress -AddressFamily IPv4 | Select-Object -Property InterfaceAlias, IPAddress, PrefixL…