C++分析AVL树

news2024/9/22 13:38:12

目录

AVL树介绍

AVL树平衡因子更新分析

AVL树插入时旋转与平衡因子更新

左单旋

右单旋

左右单旋

右左单旋

AVL旋转可行性

AVL树节点删除(待补充)

AVL树分析


AVL树介绍

二叉搜索树在某些极端情况下可能会退化,为了解决这个问题,引入了AVL树(平衡搜索二叉树中的一种)控制二叉搜索树的不平衡情况,插入一个新节点后,控制每一个节点的左右子树高度差的绝对值不超过1

之所以控制绝对值不超过1而不是不超过0是因为只有在满二叉树的情况下才可以满足每一个节点的左右子树高度差的绝对值不超过0,为了满足普遍性,选择绝对值不超过1

一棵AVL树具有以下的特点:

  1. 每一个节点的左右子树都是AVL树

  2. 左右子树高度差的绝对值(通过平衡因子判断)不超过1

绝对值不超过1,包括-1、1和0

以右子树高度减左子树高度为例,下面是含有每一个节点的高度的AVL树示意图:

AVL树平衡因子更新分析

在AVL树中,平衡因子更新只有两种情况:1. 增加 2. 减少,但是需要考虑什么时候增加什么时候减少

  1. 当插入节点在左子树时,平衡因子减少,例如插入在8节点的左子树

  2. 当插入节点在右子树时,平衡因子增加,例如插入在8的右子树

接着考虑插入新节点后,哪些节点的平衡因子需要改变。

  1. 如果插入节点在8的左子树,那么更新时只会更新8节点的平衡因子,因为8初始时是1,插入在左子树平衡因子减小,而对于节点7来说,并没有影响到其平衡因子,因为7的右子树总体的高度没有发生变化,而对于根节点的左子树以及7节点的左子树来说没有一点影响

  2. 如果按照下图的插入方式,插入节点为红色节点,此时会更新其所有祖先节点

  3. 如果插入节点在8的右子树,那么更新时8节点的平衡因子变为了2,因为8初始时是1,插入在右子树平衡因子增加,因为AVL树规定每一个节点的左右子树高度差的绝对值不能超过1,所以此时8节点需要进行额外处理使其恢复AVL树结构

综上所述,存在三种情况:

  1. 插入节点的父亲节点更新为0时:插入节点前父亲节点平衡因子为1或者-1(一边高一边矮),为1时,右子树高,插入位置为左子树;为-1时,左子树高,插入位置为右子树(插入在矮的一边),此时只需要更新父亲节点的平衡因子,因为总高度并没有发生变化。例如8节点的左子树插入节点,树的总高度还是4

  2. 插入节点的父亲节点更新为1或者-1时:插入节点前父亲节点平衡因子为0(左右子树高度相等),当更新为1时,插入位置为右子树,右子树变高,起始为-1时,插入位置为左子树,左子树变高(其中一方变高),此时需要更新所有祖先节点的平衡因子,因为插入的节点导致了树的总高度发生变化,例如例2插入红色节点树的总高度由4变为5

  3. 插入节点的父亲节点更新为2或者-2时:插入节点前父亲节点平衡因子为1或者-1(左右子树存在一方高一方矮),当更新为2时,原父亲节点平衡因子为1,并且插入位置在平衡因子为1的父亲节点的右子树,例如在节点8右子树插入新节点;当更新为-2时,原父亲节点平衡因子为-1,插入位置平衡因子为-1的父亲节点的右子树(高的一方继续变高,矮的一方没有改变),此时需要进行旋转处理将平衡因子为2或者-2的节点对应的树进行调整使其恢复AVL树结构

AVL树插入时旋转与平衡因子更新

左单旋

当插入的节点在平衡因子为1的父亲节点的右子树右子树上时,此时父亲节点的平衡因子更新为2,其右孩子节点的平衡因子分别为1和0时需要进行左单旋,例如下面的一种具体情况:

为了便于分析,将插入位置进行抽象化,a、b和c分别是高度h>=0的AVL子树,如下图所示:

此时因为父亲节点10的平衡因子变为了2,需要进行左旋,左旋需要进行的步骤如下:

平衡因子计算:

h+1(新增节点所在层数)-1=1为20节点的平衡因子

h+1+1(20节点所在层数)-h=2为10节点的平衡因子

  1. 将20的左孩子给10节点作为其右孩子

  2. 将10降下作为20的左孩子

  1. 20节点作为本棵子树的根节点

  2. 更新10和20节点的平衡因子为0

结合变量parentsubRsubRL

代码实现:

 // 左单旋
 void rotateLeft(node* parent)
 {
     // 定义节点
     node* subR = parent->_right;
     node* subRL = subR->_left;
 ​
     // 1. subRL变为parent的右孩子
     parent->_right = subRL;
     // 更新subRL的_parent为parent,需要注意当h=0时subRL节点不存在,所以需要进行判断subRL是否为空
     if (subRL)
     {
         subRL->_parent = parent;
     }
     // 2. parent变为subR的左孩子
     subR->_left = parent;
     // 3. subR变为本棵子树的根节点
     // 记录parent的父亲节点
     node* parentParent = parent->_parent;
     // 更新parent节点的父亲节点为subR
     parent->_parent = subR;
     // 更新parent节点的父亲节点的孩子节点为subR
     if (parentParent == nullptr)
     {
         // 父亲节点为空证明是根节点
         _root = subR;
         subR->_parent = nullptr;
     }
     else
     {
         if (parentParent->_left == parent)
         {
             parentParent->_left = subR;
         }
         else
         {
             parentParent->_right = subR;
         }
 ​
         // 更新subR的父亲节点
         subR->_parent = parentParent;
     }
 ​
     // 4. 更新平衡因子
     parent->_bf = subR->_bf = 0;
 }

右单旋

当插入的节点在平衡因子为-1的父亲节点的左子树左子树上时,此时父亲节点的平衡因子更新为-2,其左孩子节点的平衡因子分别为-1和0时需要进行右单旋,例如下面的一种具体情况:

为了便于分析,将插入位置进行抽象化,a、b和c分别是高度h>=0的AVL子树,如下图所示:

此时因为父亲节点10的平衡因子变为了-2,需要进行右旋,右旋需要进行的步骤如下:

平衡因子计算:

h-(h+1(新增节点所在层数))=-1为20节点的平衡因子

h-(h+1+1(20节点所在层数))=-2为10节点的平衡因子

  1. 将20的右孩子作为10的左孩子

  1. 将10作为20的右孩子

  1. 将20作为本棵子树的根节点

  2. 更新10和20的平衡因子为0

结合变量parentsubLsubLR

代码实现:

 
// 右单旋
 void rotateRight(node* parent)
 {
     node* subL = parent->_left;
     node* subLR = subL->_right;
 ​
     // 1. 将subLR作为parent的左孩子
     parent->_left = subLR;
     // 更新subLR的parent
     if (subLR)
     {
         subLR->_parent = parent;
     }
     // 2. 将parent作为subL的右孩子
     subL->_right = parent;
     // 3. 将subL作为本棵子树的根节点
     // 记录当前parent节点的父亲节点
     node* parentParent = parent->_parent;
     // 更新parent的_parent为subL
     parent->_parent = subL;
     if (parentParent == nullptr)
     {
         _root = subL;
         subL->_parent = nullptr;
     }
     else
     {
         if (parentParent->_left == parent)
         {
             parentParent->_left = subL;
         }
         else
         {
             parentParent->_right = subL;
         }
 ​
         // 更新subL的父亲节点
         subL->_parent = parentParent;
     }
 ​
     // 4. 更新平衡因子
     subL->_bf = parent->_bf = 0;
 }

左右单旋

当插入的节点在平衡因子为-1的父亲节点的左子树右子树上时,此时父亲节点的平衡因子更新为-2,其右孩子节点的平衡因子分别为1和0时需要先进行左单旋再进行右单旋,例如下面的一种具体情况:

为了便于分析,将插入位置进行抽象化

  1. a、b和c分别是高度h=0的AVL子树,如下图所示:

在当前情况下,b位置的节点即为新增节点,a和c位置此时均没有节点,10节点的平衡因子更新为-2,10节点的左孩子节点的平衡因子更新为1

平衡因子计算:

5节点的平衡因子:h+1(新增节点所在层数)-h=1

10节点的平衡因子:h-(h+1+1(5节点所在层数))=-2

  1. a、b、c和d分别是高度h>0的AVL子树,如下图所示:

h>0在上图的情况下代表已经至少存在一个节点,即5的右子树开始有一个节点存在,所以6位置没有节点时用h-1代替

此时插入节点的位置有两种:

(a) 在b位置插入,此时6节点的平衡因子变为-1,5节点的平衡因子变为1,10节点的平衡因子变为-2

平衡因子的计算:

6节点的平衡因子:h-1-h(新增节点所在层数)=-1

5节点的平衡因子:h+1(6节点所在层数)-h=1

10节点的平衡因子:h-(h+1+1)=-2

(b) 在d位置插入,此时6节点的平衡因子变为-1,5节点的平衡因子变为1,10节点的平衡因子变为-2

平衡因子的计算:

6节点的平衡因子:h+1-h(新增节点所在层数)=1

5节点的平衡因子:h+1(6节点所在层数)-h=1

10节点的平衡因子:h-(h+1+1)=-2

尽管左右单旋有两种主要情况,但是实际上影响到的平衡因子的更新,主要思路还是先左旋再右旋,为了更好观察效果,以第二种情况中的第一种情况为例分析先左旋再右旋的步骤:

  1. 左旋(将多条路径更新化为一条路径更新)

    1. 将6节点的左孩子作为5节点的右孩子

    2. 将5节点作为6节点的左孩子

    3. 将6节点作为本棵树的根节点,链接到10节点的左孩子(因为开始的父亲节点5是10节点的左孩子)

  2. 右旋(更新单一路径)

    1. 将6节点的右孩子作为10节点的左孩子

    2. 将10节点作为6节点的右孩子

    3. 将6节点作为本棵树的根节点

    4. 更新平衡因子

当树旋转结束后需要更新对应的平衡因子,此时需要考虑到两种主要情况,即h=0h>0,结合变量parentsubLsubLR

  1. h=0,左右双旋结束后如下图所示:

  2. h>0

    1. 插入位置在b,左右双旋结束后如下图所示:

    2. 插入位置在d,左右双旋结束后如下图所示:

    代码实现:

    // 左右双旋
     void rotateLR(node* parent)
     {
         // 记录旋转前的平衡因子
         node* subL = parent->_left;
         node* subLR = subL->_right;
         int bf = subLR->_bf;
     ​
         // 1. 左右双旋
         // 左旋
         rotateLeft(parent->_left);
         // 右旋
         rotateRight(parent);
     ​
         // 更新平衡因子
         if (bf == 0)
         {
             subL->_bf = parent->_bf = 0;
         }
         else if (bf == -1)// 插入在d位置
         {
             subL->_bf = 0;
             subLR->_bf = 0;
             parent->_bf = 1;
         }
         else if (bf == 1)
         {
             subL->_bf = -1;
             subLR->_bf = 0;
             parent->_bf = 0;
         }
         else
         {
             assert(false);
         }
     }

右左单旋

当插入的节点在平衡因子为1的父亲节点的右子树左子树上时,此时父亲节点的平衡因子更新为2,其右孩子节点的平衡因子分别为-1和0时需要先进行右单旋再进行左单旋,例如下面的一种具体情况:

为了便于分析,将插入位置进行抽象化

  1. a、b和c分别是高度h=0的AVL子树,如下图所示:

在当前情况下,c位置的节点即为新增节点,a和b位置此时均没有节点,10节点的平衡因子更新为-2,10节点的左孩子节点的平衡因子更新为1

  1. a、b、c和d分别是高度h>0的AVL子树,如下图所示:

h>0在上图的情况下代表已经至少存在一个节点,即20的左子树开始有一个节点存在,所以15位置没有节点时用h-1代替

此时插入节点的位置有两种:

(a)当插入在c位置时,15的平衡因子变为1,20的平衡因子变为-1,10的平衡因子变为2

(b)当插入在d位置时,15的平衡因子变为-1,20的平衡因子变为-1,10的平衡因子变为2

尽管右左单旋有两种主要情况,但是实际上影响到的平衡因子的更新,主要思路还是先右旋再左旋,为了更好观察效果,以第二种情况中的第一种情况为例分析先右旋再左旋的步骤:

  1. 右旋(将多条路径更新化为一条路径更新)

    1. 将15的右孩子作为20的左孩子

    2. 将20作为15的右孩子

    3. 15作为本棵子树的根节点,链接到10节点的右孩子位置

  2. 左旋(更新单一路径)

    1. 将15节点的左孩子作为10节点的右孩子

    2. 10节点作为15节点的左孩子

    3. 15作为本棵子树的根

    4. 更新平衡因子

当树旋转结束后需要更新对应的平衡因子,此时需要考虑到两种主要情况,即h=0h>0,结合变量parentsubRsubRL

  1. h=0,左右双旋结束后如下图所示:

  2. h>0

    1. 插入位置在c,左右双旋结束后如下图所示:

    2. 插入位置在d,左右双旋结束后如下图所示:

代码实现:

 // 右左双旋
 void rotateRL(node* parent)
 {
     // 记录旋转前的平衡因子
     node* subR = parent->_right;
     node* subRL = subR->_left;
     int bf = subRL->_bf;
 ​
     // 1. 右左双旋
     // 右旋
     rotateRight(parent->_right);
     // 左旋
     rotateLeft(parent);
 ​
     // 更新平衡因子
     if (bf == 0)
     {
         subR->_bf = parent->_bf = 0;
     }
     else if (bf == 1)// 插入在c位置
     {
         subR->_bf = 0;
         subRL->_bf = 0;
         parent->_bf = -1;
     }
     else if (bf == -1) // 插入在d位置
     {
         subR->_bf = 1;
         subRL->_bf = 0;
         parent->_bf = 0;
     }
     else
     {
         assert(false);
     }
 }

AVL旋转可行性

以左单旋为例,其余类比推理即可

在左单旋中,选择b子树作为10节点的右孩子节点是可行的,因为b子树的所有节点满足比10节点大,比20节点小,根据二叉搜索树的规则进行旋转

AVL树节点删除(待补充)

AVL树分析

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2 (N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此如果需要一种查询高效且有序的数据结构,而且数 据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合

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

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

相关文章

Redis学习[6] ——Redis缓存设计

八、Redis缓存设计 8.1 为什么Redis用作缓存? 一般来说,数据库的数据都是落在磁盘上的,会导致读写速度很慢。如果用户的请求量非常大,数据库很容易崩溃。由于Redis的数据保存在内存中,读写速度很快,所以R…

SQL注入 报错注入+附加拓展知识,一篇文章带你轻松入门

第5关--------------------------------------------> 前端直接不会显示账号密码的打印;但是在接收前端的数据的那部分后端那里,会看前端传递过来的值是否正确,如果不正确,后端接收值那里就会当MySQL语句执行错误,…

RK3568笔记五十一:W25Q64测试(spi 标准接口 )

若该文为原创文章,转载请注明原文出处。 前面有测试过W25Q64,但那是自己编写的驱动,现在使用内核自带的驱动,只需要通过SPI标准接口,编写应用程序即可以读写W25Q64. 一、硬件原理图 SPI 引脚 功能 MOSI GPIO3_C1 …

【java基础】徒手写Hello, World!程序

文章目录 前提:java环境变量配置使用vscode编写helloworld解析 前提:java环境变量配置 https://blog.csdn.net/xzzteach/article/details/140869188 使用vscode编写helloworld code .为什么用code看下图 报错了!!!&…

【MATLAB】Matlab安装包及验证生成器

通过百度网盘分享的文件:Matlab 链接: https://pan.baidu.com/s/1PF8iP31WFJUYRF7PLyiX2A?pwdxkds 提取码:xkds

简单搭建dns服务器

目录 一.安装服务 二.编写子配置文件 三.编写主配置文件 四.编写文件 五.测试 一.安装服务 [rootnode1 ~]# dnf install bind -y 二.编写子配置文件 [rootnode1 ~]# vim /etc/named.rfc1912.zones 三.编写主配置文件 [rootnode1 ~]# vim /etc/named.conf 四.编写文件 …

一款创新的物联网综合业务支撑平台,提供资费、客户、进销存、合同、订单、续费、充值、账单等功能(附源码)

前言 在当今快速发展的物联网时代,企业和开发者面临着很大的挑战和机遇。现有软件在处理物联网设备和数据管理方面常常存在一些痛点,如设备管理分散、数据同步不及时、用户交互体验不佳等。这些问题不仅影响了物联网解决方案的效率,也限制了…

docker部署可执行的jar

1.将项目打包,上传到服务器的指定目录 2.在该目录下创建Dockerfile文件 3.Dockerfile写入如下指令 # 基于哪个镜像 FROM java:8 # 拷贝文件到容器,也可以直接写成ADD xxxxx.jar /app.jar ADD springboot-file-0.0.1.jar file.jar RUN bash -c touch /…

GuLi商城-商品服务-API-新增商品-调试会员等级相关接口

在网关服务中配置路由: 代码: nacos这些服务都要启动: 如果有不是一个命名空间中的,要改成同一个命名空间中 启动商品product服务遇到循环依赖问题,解决:

AVL树在插入时保持平衡的旋转过程

目录 AVL树节点的定义 AVL树的插入 AVL树的旋转 二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。于是在这两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.…

《LeetCode热题100》---<6.①矩阵四道(二维数组)>

本篇博客讲解LeetCode热题100道矩阵篇中的四道题 第一道:矩阵置零(中等) 第二道:螺旋矩阵(中等) 第一道:矩阵置零(中等) 方法一:使用标记数组 class Solutio…

C语言指针(1)

目录 一、内存和地址 1、生活中的例子 2、内存的关系 二、指针变量和地址 1、&符号,%p占位符 2、一个简单的指针代码。 3、理解指针 4、解引用操作符 5、指针变量的大小。 三、指针变量类型的意义 1、指针解引用的作用 2、指针指针 3、指针-指针 4…

Leetcode3224. 使差值相等的最少数组改动次数

Every day a Leetcode 题目来源:3224. 使差值相等的最少数组改动次数 解法1: 想一想,什么情况下答案是 0?什么情况下答案是 1? 如果答案是 0,意味着所有 ∣nums[i]−nums[n−1−i]∣ 都等于同一个数 X。…

【JVM内存】系统性排查JVM内存问题的思路

【JVM内存】系统性排查JVM内存问题的思路 背景 前言 遇到过几次JVM堆外内存泄露的问题,每次问题的排查、修复都耗费了不少时间,问题持续几月、甚至一两年。我们将这些排查的思路梳理成一套系统的方法,希望能给对JVM内存分布、内存泄露问题…

有序矩阵中第K小的元素(LeetCode)

题目 给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。 请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。 你必须找到一个内存复杂度优于 的解决方案。 解题 from queue i…

DFS之搜索顺序与剪枝

搜索顺序: 1.https://www.acwing.com/problem/content/1119/ 首先,我们考虑一个贪心: 假如说A的倒数K个字符恰好与B的前K个字符重合,那么我们就连接。 也就是说我们一旦匹配就直接相连而不是继续找更长的重合的一段子串。 因…

秋招突击——算法练习——8/3——马上消费笔试总结——{距离为一的字符串、组合数遍历}

文章目录 引言正文第一题:距离为1的字符串个人实现修正实现 第二题:三角形数个人实现反思实现比较对象使用equalsCollections.sort方法 总结 引言 今天的笔试难度不算大,但是自己的做的很糟糕,发现了很多问题,很多模板…

目标检测,目标跟踪,目标追踪

个人专做目标检测,目标跟踪,目标追踪,deepsort。YOLOv5 yolov8 yolov7 yolov3运行指导、环境配置、数据集配置等(也可解决代码bug),cpu,gpu,可直接运行,本地安装或者远程…

JVM-类加载器和双亲委派机制

什么是类加载器? 类加载器是Jvm的重要组成之一(类加载器、运行时数据区、执行引擎、本地库接口、本地方法库),负责读取java字节码并将其加载到Jvm中的组件 类加载器的分类 Java中的类加载器可以分为以下几种: 1. 启…

Yolov8在RK3588上进行自定义目标检测(一)

1.数据集和训练模型 项目地址:https://github.com/airockchip/ultralytics_yolov8.git 从github(htps:l/github.com/airockchip/ultralytics_yolov8)上获取yolov8模型。 下载项目: git clone https://github.com/airockchip/ultralytics_yolov8.git …