【数据结构】平衡二叉树

导语

对于二叉搜索树 而言,它的 增、 删 、 改 、 查  的时间复杂度为 O(\log_{2}n) ~ O(n) 。原因是最坏情况下,二叉搜索树会退化成 线性表  。换言之,树的高度决定了它插入、删除和查找的时间复杂度
为了对上述缺点进一步优化,设计了一种高度始终能够接近 O(\log_{2}n) 的 树形  的数据结构,它既有链表的快速插入与删除的特点,又有顺序表快速查找的优势。它就是:平衡二叉树 。


 一、平衡二叉树基本概念

1、平衡二叉树的定义

平衡二叉树(AVL树),是一种平衡(balanced)二叉搜索树(binary search tree, 简称为BST)。由两位科学家在1962年发表的论文《An algorithm for the organization of information》当中提出,作者是发明者G.M. Adelson-Velsky和E.M. Landis。它具有以下两个性质:

  • 空树是平衡二叉树
  • 任意一个结点的key,比它的左孩子key大,比它的右孩子key小;
  • 任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。

2、树的高度

一棵树的高度,是指从树根结点到达最远的叶子结点的路径上经过的结点数。所以,求树的高度我们可以采用递归的方式。主要有以下三种情况:

1)空树的树高为 0;

2)叶子结点的树高为 1;

3)其它结点的树高,等于左右子树树高的大者加 1;

3、平衡因子

二叉树上的结点的 左子树高度 减去 右子树高度 的值称为 平衡因子,即 BF(Balance Factor)。 根据平衡二叉树的定义,树上所有结点的平衡因子只可能是 -1、0 和 1。即只要二叉树上有一个结点的平衡因子的绝对值大于 1,则该二叉树就是不平衡的。

二、平衡二叉树存储结构

对于平衡二叉树,首先是二叉搜索树,所以会有 左右孩子指针数据域,然后特殊之处是需要平衡因子,而平衡因子可以通过节点树的高度来计算,所以需要加一个 高度。

struct TreeNode {int val;struct TreeNode* left;struct TreeNode* right;int height;TreeNode(int x, int h = 1) :val(x), left(nullptr), right(nullptr), height(h){}
};

三、平衡二叉树基本接口

1、获取树高

int AVLGetHeight(TreeNode* node)
{if (node == nullptr) {return 0;}return node->height;
}

获取树高期望做到 O(1) 的时间复杂度,height 字段是需要存储在结点上的,并且每次树的 插入、删除 操作都需要更新这个值

空结点的树高为 0,其它结点的树高可以直接通过获取树结点结构体的成员变量height 字段快速获取。

2、计算树高

每次对树进行插入、删除操作,对树的原结点高度会有影响,所以需要重新计算,更新这个值。

//计算树高(结点增删需要重新计算树高)
void AVLCalcHeight(TreeNode* node) {if (nullptr == node) {              return ;}                                node->height = 1 + std::max(AVLGetHeight(node->left), AVLGetHeight(node->right));
}

 3、获取平衡因子

同理,每次对树进行插入、删除操作,对树的原结点的平衡因子也会有影响,所以也需要重新计算这个值。

//获取平衡因子
int AVLGetBalanceFactor(TreeNode* node) {if (node == nullptr)return 0;                                                return AVLGetHeight(node->left) - AVLGetHeight(node->right); 
}

4、旋转操作

每次对树进行插入、删除操作,可能会引起树的平衡,此时就需要通过旋转操作来使树重新回到平衡状态。

假设本来这棵树是平衡的,在我们在插入一个结点以后,导致了这棵树的不平衡,那么必然是这棵树根结点的平衡因子从 +1 变成了 +2,或者从 -1 变成了 -2 。我们来分别讨论这两种情况。

实际上,总共有四种情况:

1)LL(往左子树的左子树插入一个结点),根结点的平衡因子 +2,左子树根结点平衡因子 +1;

2)RR(往右子树的右子树插入一个结点),根结点的平衡因子 -2,右子树根结点平衡因子 -1;

3)LR(往左子树的右子树插入一个结点),根结点的平衡因子 +2,左子树根结点平衡因子 -1;

4)RL(往右子树的左子树插入一个结点),根结点的平衡因子 -2,右子树根结点平衡因子 +1;

结论:+1 变成 +2 的情况发生在 LL 和 LR,即往当前树的左子树插入一个结点的情况;-1 变成 -2 的情况发生在 RL 和 RR,即往当前树的右子树插入一个结点的情况。

(1) LL

LL,即往左子树的左子树插入一个结点。这种情况下,如果树出现了不平衡的情况,根结点的当前平衡因子一定是 +2。

如上图所示,在左子树插入T5结点后,平衡二叉树的平衡状态被打破,要想回到平衡状态需要对树进行一个右旋操作。

如图所示,以左子树根结点T1作支点右旋后,重新达到平衡。总共有以下关系发生了变化:

(1)T1变成了新的树根

(2)T和T1父子关系发生了交换

(3)T4的父节点由T1变为T

右旋源码

//右旋
TreeNode* RRotate(TreeNode* T)
{TreeNode* LNode = T->left;T->left = LNode->right;LNode->right = T;AVLCalcHeight(T);AVLCalcHeight(LNode);return LNode;
}

经过右旋后,只有T1和T的高度发生了变化,所以需要对它们重新计算高度。

LL型旋转处理

// LL 型右旋处理
TreeNode* AVLTreeLL(TreeNode* T) {return RRotate(T);
}
(2)RR

RR,即往右子树的右子树插入一个结点。这种情况下,如果树出现了不平衡的情况,根结点的当前平衡因子一定是 -2。

如上图所示,在右子树插入T5结点后,平衡二叉树的平衡状态被打破,要想回到平衡状态需要对树进行一个左旋操作。

如图所示,以右子树根结点T2作支点左旋后,重新达到平衡。总共有以下关系发生了变化:

(1)T2变成了新的树根

(2)T和T2父子关系发生了交换

(3)T3的父节点由T2变为T

左旋源码

//左旋
TreeNode* LRotate(TreeNode* T)
{TreeNode* RNode = T->right;T->right = RNode->left;RNode->left = T;AVLCalcHeight(T);AVLCalcHeight(RNode);return RNode;
}

RR型处理源码

//RR型左旋处理
TreeNode* AVLTreeRR(TreeNode* T) {return LRotate(T);
}
(3)LR

LR,即往左子树的右子树插入一个结点。这种情况下,如果树出现了不平衡的情况的话,根结点的当前平衡因子一定是 +2。

假设以左子树的右子树T4结点为支点,对左子树进行一次左旋操作,得到如下图所示:

可以看到,经过一次左旋得到新树,形状和LL型一致,所以接下来再按照型LL处理,再右旋一次即可达到平衡状态

所以对于LR型的处理主要有两步:

(1)对树T的左子树进行左旋

(2)对树T进行右旋

LR型处理源码

//LR型左旋+右旋处理
TreeNode* AVLTreeLR(TreeNode* T) {T->left = LRotate(T->left);   //左旋处理并修改T的左指针指向return RRotate(T);    //对T进行右旋处理       
}
 (4)RL

RL,即往右子树的左子树插入一个结点。这种情况下,如果树出现了不平衡的情况的话,根结点的当前平衡因子一定是 -2。

假设以右的左子树T4结点为支点,对右子树进行一次右旋操作,得到如下图所示:

可以看到,经过一次右旋得到新树,形状和RR型一致,所以接下来再按照型RR型处理,再左旋一次即可达到平衡状态

所以对于RL型的处理主要有两步:

(1)对树T的右子树进行右旋

(2)对树T进行左旋

RL型处理源码

//RL型右旋+左旋处理
TreeNode* AVLTreeRL(TreeNode* T) {T->right = RRotate(T->right);    // 右子树进行右旋处理并修改T的右指针指向return LRotate(T);               // 对T树进行左旋处理
}

四、平衡二叉树基本操作

1、查找

(1)查找定值

对于要查找的数据 data,从根结点出发,每次选择左子树或者右子树进行查找, n 个结点的树高最多为\log {_{2}}^{n},所以查找的时间复杂度为 O(\log {_{2}}^{n}) ,总共四种情况依次进行判断:

1)若为空树,直接返回 false;

2) data 小于 树根结点的数据域,说明 data 对应的结点不在根结点,也不在右子树上,则递归返回左子树的 查找 结果;

3) data 大于 树根结点的数据域,说明 data 对应的结点不在根结点,也不在左子树上,则递归返回右子树的 查找 结果;

4) data 等于 树根结点的数据域,则直接返回 true ;

bool AVLFind(TreeNode* T, int data) {if (T == nullptr) {return false;                        // 空树}if (data < T->val) {return AVLFind(T->left, data);       // data<val,递归查找左子树}else if (data > T->val) {return AVLFind(T->right, data);      //  data>val,递归查找右子树}return true;                             //  data=val
}
(2)查找最小值结点

迭代找到树的最左结点即可。

//找最小值结点
TreeNode* AVLGetMin(TreeNode* T) {while (T && T->left)   T = T->left;       return T;              
}
(3)查找最大值

迭代找到树的最右结点即可。

//找最大值结点
TreeNode* AVLGetMax(TreeNode* T) {while (T && T->right)  T = T->right;      return T;              
}

2、平衡

每次当我们对树的结点进行插入或者删除的时候,都有可能打破树的平衡性,这时候就需要一些旋转操作来使树重新恢复平衡。 究竟是进行左旋,右旋,还是双旋,就要通过平衡因子来判断了。

令根结点为 T ,左子树的根结点为 L ,右子树的根结点为 R , \displaystyle T_{bf}代表根结点的平衡因子,\displaystyle L{_{bf}}代表左子树根的平衡因子, R_{bf}代表右子树根的平衡因子。总共分为以下四种情况:

1)  \displaystyle T_{bf} > 1 , \displaystyle L{_{bf}} > 0 ,则为 LL 型,需要进行一次右旋;

2)  \displaystyle T_{bf} > 1 , \displaystyle L{_{bf}} ≤ 0 ,则为 LR 型,需要进行一次双旋;

3)  \displaystyle T_{bf} < −1 , R_{bf} > 0 ,则为 RL 型,需要进行一次双旋;

4)  \displaystyle T_{bf} < −1 , R_{bf} ≤ 0 ,则为 RR 型,需要进行一次左旋

 平衡源码

//平衡选转
TreeNode* AVLBalance(TreeNode* T) {int bf = AVLGetBalanceFactor(T);if (bf > 1) {if (AVLGetBalanceFactor(T->left) > 0)T = AVLTreeLL(T);                 // LL型,右旋一次elseT = AVLTreeLR(T);                 // LR型,左旋+右旋 }if (bf < -1) {if (AVLGetBalanceFactor(T->right) > 0)T = AVLTreeRL(T);                 // RL型,右旋+左旋 elseT = AVLTreeRR(T);                 // RR型,左旋一次}AVLCalcHeight(T);                         // 重新计算根结点高度,因为之前旋转时并未完成相关操作return T;                                 
}

3、插入

对于要插入的数据 data ,从根结点出发,分情况依次判断:

1)若为空树,则创建一个值为 data 的结点并且返回;

2) data 的值 等于 树根结点的值,无须执行插入,直接返回根结点;

3) data 的值 小于 树根结点的值,那么插入位置一定在 左子树,递归执行插入左子树的过程,并且返回插入结果作为新的左子树

4) data 的值 大于 树根结点的值,那么插入位置一定在 右子树,递归执行插入右子树的过程,并且返回插入结果作为新的右子树

最后,在3或4情况执行完成后,需要对树执行 平衡 操作。

插入源码

TreeNode* AVLInsert(TreeNode* T, int data) {if (T == nullptr) {T = new TreeNode(data);               // 空树,创建val=data的结点return T;}if (data == T->val) {return T;                              // data已经存在}else if (data < T->val) {T->left = AVLInsert(T->left, data);    // 递归查找左子树适合位置,插入 }else {T->right = AVLInsert(T->right, data);  // 递归查找右子树适合位置,插入 }return AVLBalance(T);                      // 重新平衡 
}

4、删除

(1)删除根结点

对一棵平衡二叉树,删除它的根结点,需要保证它还是一棵二叉平衡树,则有如下四种情况:

1)空树,无须执行删除,直接返回空;

2)只有左子树时,将根结点空间释放后,返回左子树;

3)只有右子树时,将根结点空间释放后,返回右子树;

4)当左右子树都有时,根据左右子树的平衡性分情况讨论:如果左子树更高,则从左子树选择最大值替换根结点,并且递归删除左子树对应结点;右子树更高,则从右子树选择最小值替换根结点,并且递归删除右子树对应结点;

5)最后,重新计算所有树高,并且返回根结点;

//删除根结点
TreeNode* AVLRemoveRoot(TreeNode* T) {TreeNode* delNode = nullptr;TreeNode* retNode = nullptr;if (T == nullptr) {return nullptr;                 // 空树,直接返回 }if (T->right == nullptr) {    // 只有左子树(包含单节点情况),释放根结点空间后,返回左子树根结点retNode = T->left;delete T;}else if (T->left == nullptr) {    // 只有右子树,释放根结点空间后,返回右子树根结点 retNode = T->right;delete T;}else {								// 左右子树都存在 if (AVLGetHeight(T->left) > AVLGetHeight(T->right)) {  // 左子树高于右子树retNode = T;//获取左子树最大值结点,并以它的值作为根结点的新值TreeNode* cur = T->left;TreeNode* pcur = T;while (cur->right){pcur = cur;cur = cur->right;}delNode = cur;retNode->val = cur->val;if (pcur->right == cur) {//左子树的最大值在左子树的右子树上pcur->right = cur->left;}else {//左子树的最大值为左子树的根pcur->left = cur->left;}delete delNode;retNode = AVLBalance(T);AVLCalcAllHeight(retNode);}else {   // 右子树高于左子树retNode = T;//获取右子树最小值结点,并以它的值作为根结点的新值TreeNode* cur = T->right;TreeNode* pcur = T;while (cur->left){pcur = cur;cur = cur->left;}delNode = cur;retNode->val = cur->val;if (pcur->left == cur) {//右子树的最小值在右子树的左子树上pcur->left = cur->right;}else {//右子树的最小值为右子树的根pcur->right = cur->right;}delete delNode;retNode = AVLBalance(T);AVLCalcAllHeight(retNode);}}return retNode;
}
(2)删除指定结点

删除值为 data 的结点的过程,从根结点出发,总共四种情况依次判断:

1)空树,不存在结点,直接返回空 ;

2) data 的值 等于 树根结点的值,则调用 删除根结点 的接口,这个过程下文会详细介绍;

3) data 的值 小于 树根结点的值,则需要删除的结点一定不在右子树上,递归调用删除左子树的对应结点,并且将删除结点返回的子树作为新的左子树;

4) data 的值 大于 树根结点的值,则需要删除的结点一定不在左子树上,递归调用删除右子树的对应结点,并且将删除结点返回的子树作为新的右子树;

5)最后,对于 3) 和 4) 这两步,需要对树执行 平衡 操作。

TreeNode* AVLRemove(TreeNode* root,int val)
{if (nullptr == root) {return nullptr;}if (val == root->val) {return AVLRemoveRoot(root);}else if (val < root->val) {root->left = AVLRemove(root->left, val);}else if (val > root->val) {root->right = AVLRemove(root->right, val);}root = AVLBalance(root);AVLCalcAllHeight(root);return root;
}

五、平衡二叉树的缺点

由于AVL树必须保证左右子树平衡,Max(最大树高-最小树高) <= 1,所以在插入的时候很容易出现不平衡的情况,一旦这样,就需要进行旋转以求达到平衡。

正是由于这种严格的平衡条件,导致AVL需要花大量时间在调整上,故AVL树一般使用场景在于查询场景, 而不是 增加、删除频繁的场景。

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

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

相关文章

APM32F035有感矢量控制方案

一、先来几句废话 首先这两年公司越来越多的开始使用国产的MCU&#xff0c;用过GD32、AT32、APM32等等&#xff0c;目前稳定使用的是APM32,包括身边朋友工作室&#xff0c;也开始从TI、STM、NXP换成APM32。上个月有幸拿到APM32F035电路控制板&#xff0c;非常感谢面包板社区提供…

实战环境搭建-linux下安装tomcat

安装tomcat Index of /dist/tomcat/tomcat-9/v9.0.8/bin 下载apache-tomcat-9.0.8.tar.gz,可以使用wget; 2、将压缩包tar -zxvf apache-tomcat-9.0.8.tar.gz解压到/home/tomcat 3、修改环境变量 vi /etc/profile export JAVA_HOME=/home/java/jdk1.8.0_221 export JRE_HO…

sublime如何取消运行代码状态

sublime如何取消运行代码状态 解决方案待续、更新中 解决方案 1 顶部取消: 工具-----取消编译 这个看自己编译器sublime取消编译是否可用,可用则用 ,否则使用下面方法 2 底部栏取消–如图所示: 取消成功: 待续、更新中 ————————————————————— 以上就…

基于php应用的文件管理器eXtplorer部署网站并内网穿透远程访问

文章目录 1. 前言2. eXtplorer网站搭建2.1 eXtplorer下载和安装2.2 eXtplorer网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1. 前言 通过互联网传输文件&#xff0c;是互联网最重要的应用之一&#xff0c;无论是…

7.数据转换、格式化、校验

日期字符串格式的表单参数,提交后转换为 Date 类型 <!-- 解决问题: 1.数据类型转换 2.数据格式 3.数据校验 --> BirthDay :<form:input path="birthDay"/>Employee 类中增加日期类型属性: //关于类型转换 private Date birthDay ;数据绑定流程原理 …

Qt QWidget窗口基类

文章目录 1 QWidget介绍2 如何显示 QWidget窗口2.1 新建基于QWidget的窗口类2.2 再添加一个QWidget窗口类2.3 显示新添加的 QWidget窗口 3 常用的属性和方法3.1 窗口位置3.2 窗口大小3.3 窗口标题3.4 窗口图标3.5 资源文件 4 实例 1 QWidget介绍 Qt 中的常用控件&#xff0c;比…

什么是CDN,优势在哪里

随着互联网的普及和用户需求的多样化&#xff0c;网站的速度和稳定性已经成为影响用户体验的关键因素。CDN加速作为解决这一问题的有效手段&#xff0c;正逐渐受到业界的广泛关注。 为什么说对网站这一块起到这么关键性的作用呢&#xff1f;它的优势在哪&#xff1f; 1.提升网…

【机器学习】卷积神经网络(五)-计算机视觉应用

七、应用-计算机视觉 7.1 人脸检测 DenseBox\Femaleness-Net\MT-CNN\Cascade CNN 介绍 VJ框架的分类器级联用于卷积网络 用于人脸检测的紧凑卷积神经网络级联 问题&#xff1a;作者希望实时检测高分辨率视频流中的正面&#xff0c;由于人脸图像和背景的多样性和复杂性&#xff…

【MIdjourney】图像角度关键词

本篇仅是我个人在使用过程中的一些经验之谈&#xff0c;不代表一定是对的&#xff0c;如有任何问题欢迎在评论区指正&#xff0c;如有补充也欢迎在评论区留言。 1.侧面视角(from side) 侧面视角观察或拍摄的主体通常以其侧面的特征为主要焦点&#xff0c;以便更好地展示其轮廓…

02. Eureka、Nacos注册中心及负载均衡原理

01小节中订单服务远程调用用户服务案例实现了跨服务请求&#xff0c;在微服务中一个服务可能是集群部署的&#xff0c;也就是一个服务有多个实例&#xff0c;但是我们在调用服务时需要指定具体的服务实例才能调用该服务&#xff0c;在集群模式下&#xff0c;服务地址应该写哪个…

1.3号io网络

文件IO 1.文件IO是基于系统调用 2.程序每进行一次系统调用&#xff0c;就会从用户空间向内核空间进行一次切换&#xff0c;执行效率较慢 3.目的&#xff1a;由于后期进程间通信&#xff0c;如管道、套接字通信&#xff0c;都使用的是文件IO&#xff0c;所以引入文件IO操作的…

MATLAB根据数据拟合曲线

MATLAB根据数据拟合曲线 MATLAB根据数据拟合曲线视频观看 MATLAB根据数据拟合曲线 x1[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,6…

C语言之详解数组【附三子棋和扫雷游戏实战】

文章目录 一、一维数组的创建和初始化1、数组的创建2、数组的初始化3、一维数组的使用4、 一维数组在内存中的存储 二、二维数组的创建和初始化1、二维数组的创建2、二维数组的初始化3、二维数组的使用4、二维数组在内存中的存储 三、数组越界边界值考虑不当导致越界访问数组大…

Mysql数据库的基础操作

1、数据库的数据类型和结构设置&#xff0c;修改等 DML&#xff1a;针对数据的增删改 where条件更像是这一条命令中的限制条件&#xff0c;如果不带where条件的时候&#xff0c;相当于针对全表所有字段进行操作 DQL&#xff1b; 数据查询语言 1、查询关键词使用 select 这个里…

MYSQL学习之buffer pool的理论学习

MYSQL学习之buffer pool的理论学习 by 小乌龟 文章目录 MYSQL学习之buffer pool的理论学习前言一、buffer pool是什么&#xff1f;二、buffer pool 的内存结构三、buffer pool 的初始化和配置初始化配置 四、buffer pool 空间管理LRU淘汰法冷热数据分离的LRU算法1.引入库2.读入…

MacBook Pro M1搭建Kafka2.7版本源码运行环境

原创/朱季谦 最近在阅读Kafka的源码&#xff0c;想可以在阅读过程当中&#xff0c;在代码写一些注释&#xff0c;便决定将源码部署到本地运行。 日常开发过程中&#xff0c;用得比较多一个版本是Kafka2.7版本&#xff0c;故而在MacBook Pro笔记本上用这个版本的源码进行搭建&…

计算机网络实验(二):Wireshark网络协议分析

一、实验名称&#xff1a;Wireshark网络协议分析 二、实验原理 HTTP协议分析 1.超文本传输协议&#xff08;Hypertext Transfer Protocol, HTTP&#xff09;是万维网&#xff08;World Wide Web&#xff09;的传输机制&#xff0c;允许浏览器通过连接Web服务器浏览网页。目…

高性能、可扩展、支持二次开发的企业电子招标采购系统源码

在数字化时代&#xff0c;企业需要借助先进的数字化技术来提高工程管理效率和质量。招投标管理系统作为企业内部业务项目管理的重要应用平台&#xff0c;涵盖了门户管理、立项管理、采购项目管理、采购公告管理、考核管理、报表管理、评审管理、企业管理、采购管理和系统管理等…

2023我的编程之旅、2024新的启程

目录 一、2023年结束、2024年开始 1、回顾2023年 1.1、发表文章概述 1.2、开发中遇到的问题与解决方案 2、展望2024年 2.1、新年Flag 2.2、收获与成长 一、2023年结束、2024年开始 光阴荏苒&#xff0c;从我开始在CSDN写作已经2年零5个月了&#xff0c;我也在不断的思考…

关于使用统一服务器,vscode和网页版jupyter notebook的交互问题

autodl 查看虚拟环境 在antodl上租借了一个服务器&#xff0c;通过在网页上运行jupyter notebook和在vscode中运行&#xff0c;发现环境都默认的是miniconda3。 conda info --envs 当然环境中所有的包都是一样的。 要查看当前虚拟环境中安装的所有包&#xff0c;可以使用以…