高阶DS---AVL树详解(每步配图)

news2025/1/14 18:14:05

目录

前言:

AVL树的概念:

AVL树节点的定义:

AVL树的插入(重点)

AVL树的旋转:

(1)新节点插入较高左子树的左侧---右单旋

(2)新节点插入较高右子树的右侧---左单旋

(3)新节点插入较高左子树的右侧---左右双旋

(4)新节点插入较高右子树的左侧---右左双旋

总结:

AVL树的验证:

验证用例:

AVL树的删除(了解):

AVL树性能分析:

结语:


前言:

如果有友友需要本文章的全部源码的话请前往AVL树源码

AVL树的概念:

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过 1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

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

(1)它的左右子树都是AVL树

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

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(logN) ,搜索时间复杂度O(logN)。

例如下图就是一个AVL树圆圈外面的数字就是平衡因子,为右子树高度 - 左子树高度。

AVL树节点的定义:

为了AVL树实现简单,AVL树节点在定义时维护一个平衡因子和采用孩子双亲表示法,具体节点定义如下:

因为在实际开发时我们都是树节点单独创建一个类不是很经常使用静态内部类,故我们这里就创建一个TreeNode类来实现。

public class TreeNode {
    public int val;//节点值
    public int bf;//AVL树的平衡因子
    public TreeNode left;//左孩子引用
    public TreeNode right;//右孩子引用
    public TreeNode parent;//父亲节点引用
    public TreeNode(int val){
        this.val = val;
    }

}

注意:

当前节点的平衡因子 = 右子树高度 - 左子树的高度。但是,不是每棵树,都必须有平衡因子,这只是其中的一种实现方式。

AVL树的插入(重点)

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

(1)按照二叉搜索树的方式插入新节点。

(2)调整节点的平衡因子。

但是在插入节点后要更新平衡因子,这时AVL树的平衡性就可能会遭到破坏,我们就要进行调整。

假设插入节点为node,node的父亲节点为parent,在node节点插入后,parent节点的平衡因子一定要进行调整,在插入之前,parent的平衡因子分为三种情况:-1,0,1。

(1)如果cur插入到parent的左侧,只需给parent的平衡因子 -1 即可。

(2)如果cur插入到parent的右侧,只需给parent的平衡因子 +1 即可。

此时:parent的平衡因子可能有三种情况:0,正负1, 正负2。对应分析如下:

(1)如果parent的平衡因子为0,说明插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功,就不需要向上调整,因为对于上面的节点来说这颗子树的最大高度没有改变。

(2)如果parent的平衡因子为正负1,说明插入前parent的平衡因子一定为0,插入后被更新成正负1,此时以parent为根的树的高度增加,需要继续向上更新。

(3)如果parent的平衡因子为正负2,则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理。

根据上面的分析我们可以先写出如下代码(大体框架),首先我们先根据二叉搜索树的查找节点方式找到要插入节点的父亲节点,插入节点后修改平衡因子,根据修改后平衡因子的情况分为三种情况。👍👍👍

public boolean insert(int val){
        //根据二叉搜索树查找节点的方式,找到插入点
        TreeNode node = new TreeNode(val);
        //根节点为空
        if(root == null){
            root = node;
        }
        TreeNode cur = root;
        TreeNode parent = null;
        //parent始终是cur的父亲节点,当cur为null时parent就是我们要插入节点的父亲节点
        while(cur != null){
            if(cur.val > val){
                //去左子树找
                parent = cur;
                cur = cur.left;
            }else if(cur.val < val){
                //去右子树找
                parent = cur;
                cur = cur.right;
            }else{
                //插入节点已存在,插入失败
                return false;
            }
        }
        //插入节点
        if(parent.val < val){
            parent.right = node;
        }else{
            parent.left = node;
        }
        node.parent = parent;
        cur = node;
        //调整插入节点父亲节点的平衡因子
        while(parent != null){
            if(cur == parent.left){
                parent.bf--;
            }else{
                parent.bf++;
            }
            //当调整后父亲节点平衡因子为0
            if(parent.bf == 0){
                //说明插入后,parent树的左右最大深度不变
                //已经平衡了
                break;
            }else if(parent.bf == 1 || parent.bf == -1){
                //说明插入后,parent树的左右最大深度改变,会影响parent树的parent的平衡因子
                //要继续向上修改平衡因子
                cur = parent;
                parent = cur.parent;
            }else{
                //parent的平衡因子为2,要进行旋转调整
                //有两种情况分别为右树高和左树高
                if(parent.bf == 2){
                    if(cur.bf == 1){
                        //左旋
                        rotateLeft(parent);
                    }else{
                        //进行右左双旋
                        //其实就是先调整成能左旋的情况再左旋
                        //cur.bf == -1
                        rotateRL(parent);
                    }

                }else{
                    //parent.bf == -2左树高
                    if(cur.bf == -1){
                        //右旋
                        rotateRight(parent);
                    }else{
                        //左右双旋
                        //先左旋成能将整体进行右旋的情况再进行右旋
                        rotateLR(parent);
                        //cur.bf == 1
                    }
                }
                //
                break;
            }
        }
        return true;
    }

如果对上面代码的else部分为什么是这么旋转感到疑惑的话,可以先跳过,在介绍完下面AVL树的旋转后就明白了。 

AVL树的旋转:

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

注意:下面我画的四张图大家一定一定要会画,总不可能记代码吧,没有意义。

我最常用的例子节点是:60,30,a,b,c(其中a,b,c为满足要求的任意值)

(1)新节点插入较高左子树的左侧---右单旋

上面的图便是我们右单旋的全过程了,我们可以发现旋转完后这个AVL树变得平衡了,且需要修改的平衡因子只有两个subL和parent的平衡因子。

具体代码如下:

在这过程中subLR节点可能不存在故要来一次特判防止空指针异常,接着要分是根节点和不是根节点两种情况,如果不是根节点又要分是pParent(parent的父亲节点)的左子树还是右子树。主要要弄好指向和平衡因子的修改(经过分析和作图后发现调整后只有subL和parent平衡因子发生改变)。

private void rotateRight(TreeNode parent){
        //右旋
        TreeNode subL = parent.left;//parent节点的左孩子节点
        TreeNode subLR = subL.right;//parent节点的左孩子节点的右孩子节点
        parent.left = subLR;
        //防止节点不存在,空指针异常
        if(subLR != null){
            subLR.parent = parent;
        }
        subL.right = parent;
        //在修改parent的父亲节点时,要提前记入下来防止丢失
        TreeNode pParent = parent.parent;
        parent.parent = subL;
        //如果parent是根节点,即没有父亲节点
        if(parent == root){
            root = subL;
            subL.parent = null;
        }else{
            //有父亲节点故要考虑其是父亲节点的左孩子还是右孩子
            if(pParent.left == parent){
                pParent.left = subL;
            }else{
                pParent.right = subL;
            }
            subL.parent = pParent;
        }
        //经过分析和作图发现调整后只有subL和parent平衡因子发生改变
        //subL平衡因子从-1变成0
        //parent平衡因子从-2变成0
        subL.bf = 0;
        parent.bf = 0;
    }

(2)新节点插入较高右子树的右侧---左单旋

上图为我们左单旋的全过程,这个其实可以仿照我们右单旋的步骤来。具体代码如下:

用private进行封装,创建对应的孩子节点,进行下面个个参数的指向修改时一定要画图🌸🌸🌸,节点的选取可以仿照我上面画的,最后注意不要忘了修改对应节点的平衡因子。

这里教给大家一个记忆小技巧:对哪个节点进行旋转,新的parent节点的旋转方向(根据名字)节点要断掉。

private void rotateLeft(TreeNode parent){
        //左旋
        //小技巧:对哪个节点进行旋转,新的parent节点的旋转方向(根据名字)节点要断掉
        TreeNode subR = parent.right;//parent的右孩子
        TreeNode subRL = subR.left;//parent的右孩子的左孩子
        //防止节点不存在空指针异常
        if(subRL != null){
            subRL.parent = parent;
        }
        //这里建议画图理解
        parent.right = subRL;
        subR.left = parent;
        //在修改parent的父亲节点时,要提前记入下来防止丢失
        TreeNode pParent = parent.parent;
        parent.parent = subR;
        if(parent == root){
            root = subR;
            subR.parent = null;
        }else{
            if(pParent.left == parent){
                pParent.left = subR;
            }else{
                pParent.right = subR;
            }
            subR.parent = pParent;
        }
        subR.bf = 0;
        parent.bf = 0;
    }

(3)新节点插入较高左子树的右侧---左右双旋

在有些情况下只进行左旋和右旋还并不能解决所有情况,例如下图,如果友友感兴趣的话可以自己试试,显然一次旋转完成不了。我们正确的旋转方式为左右双旋,先左旋再进行右旋。

正确旋转过程如下图。

下图只演示了subLR的平衡因子为-1的情况还有1和0的情况就交给友友们自己去完成了都差不多的👍👍👍 

代码如下:

这里特别注意我们传入左右旋的方法的参数是传入parent而不是其孩子节点,这个一定要弄清楚否则就错了,下面之所以没有bf == 0的情况是因为在 bf == 0 的情况下在rotateLeft方法和rotateRight方法下就已经吧要修改的bf修改完成了。但是不能if后面用else必须是else if,因为else会把bf == 0的情况收纳进去这样就出错了。 

private void rotateLR(TreeNode parent){
        //左右双旋
        TreeNode subL = parent.left;
        TreeNode subLR = subL.right;
        int bf = subLR.bf;
        //bf的获取必须在旋转之前否则会因旋转而改变,旋转会改变对应的平衡因子
        rotateLeft(subL);
        rotateRight(parent);
        //画图,这里可以分为插在左边还是右边
        if(bf == -1){
            parent.bf = 1;
            subL.bf = 0;
            subLR.bf = 0;
        }else if(bf == 1){
            //bf == 1
            parent.bf = 0;
            subLR.bf = 0;
            subL.bf = -1;
        }
    }

(4)新节点插入较高右子树的左侧---右左双旋

右左双旋的实现,友友们可以参考左右双旋。

具体流程和左右双旋差不多也有三种情况,bf为0,1,-1的三种情况,注意传入左右旋方法的参数是传入对应的parent节点。

对应代码如下:

private void rotateRL(TreeNode parent){
        //右左双旋
        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;
        int bf = subRL.bf;
        //bf的获取必须在旋转之前否则会因旋转而改变
        //旋转传入的父亲节点为原来的不是改变后的
        rotateRight(subR);
        rotateLeft(parent);
        //画图,这里可以分为插在左边还是右边
        if(bf == 1){
            parent.bf = -1;
            subRL.bf = 0;
            subR.bf = 0;
        }else if(bf == -1){
            // bf == -1
            subRL.bf = 0;
            parent.bf = 0;
            subR.bf = 1;
        }
    }

总结:

新节点插入后,假设以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑:

1.pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR。

(1)当pSubR的平衡因子为1时,执行左单旋。

(2)当pSubR的平衡因子为-1时,执行右左双旋。

2.pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL。

(1)当pSubL的平衡因子为-1是,执行右单旋。

(2)当pSubL的平衡因子为1时,执行左右双旋。

即:pParent与其较高子树节点的平衡因子时同号时单旋转,异号时双旋转。

旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

AVL树的验证:

分为两步:

(1)验证其为二叉搜索树

如果中序遍历可得到一个有序的序列,就说明为二叉搜索树

(2)验证其为平衡树

1.每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)

2.节点的平衡因子是否计算正确

对应代码如下:

public boolean isBalance(TreeNode root){
        if(root == null) return true;
        int heightL = height(root.left);
        int heightR = height(root.right);
        if(heightR - heightL != root.bf){
            System.out.println(root.val + ":的平衡因子计算错误");
            return false;
        }
        return Math.abs(heightL - heightR) <= 1 && isBalance(root.left) && isBalance(root.right);
    }
    private int height(TreeNode root){
        if(root == null) return 0;
        int heightL = height(root.left);
        int heightR = height(root.right);
        return Math.max(heightL,heightR) + 1;
    }
    public void inOrder(TreeNode root){
        if(root == null) return;
        inOrder(root.left);
        System.out.print(root.val + " ");
        inOrder(root.right);
    }

验证用例:

大家可以自己完成AVL树的代码后把下面这三个实例带进去验证,如果是true且中序遍历为升序的话代码就没什么问题了。

这里再补充一个用例:int[] array = {30,20,90,60,180,40};

AVL树的删除(了解):

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与删除不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。

这里由于实现比较麻烦要考虑的东西很多(且面试一般也不会让你写代码)文章篇幅有限只说大致流程:

1、找到需要删除的节点。

2、按照搜索树的删除规则删除节点。

3、更新平衡因子,如果出现了不平衡,进行旋转。单旋,双旋。

AVL树性能分析:

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

结语:

其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

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

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

相关文章

经纬恒润AUTOSAR产品成功适配芯来RISC-V车规内核

近日&#xff0c;经纬恒润AUTOSAR基础软件产品INTEWORK-EAS&#xff08;ECU AUTOSAR Software&#xff0c;以下简称EAS&#xff09;在芯来提供的HP060开发板上成功适配芯来科技的RISC-V处理器NA内核&#xff0c;双方携手打造了具备灵活、可靠、高性能、强安全性的解决方案。这极…

【嵌入式智能产品开发实战】(十二)—— 政安晨:通过ARM-Linux掌握基本技能【C语言程序的安装运行】

目录 程序的安装 程序安装的本质 在Linux下制作软件安装包 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: 嵌入式智能产品开发实战 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xf…

Python学习之-协程

前言&#xff1a; 在Python中&#xff0c;协程(coroutines)是利用生成器(generator)的特性&#xff0c;来实现并发编程的一种方式。从Python 3.5开始&#xff0c;通过引入async和await关键字&#xff0c;Python对异步IO提供了更原生的支持&#xff0c;使得协程成为了实现异步编…

脑机辅助推导算法

目录 一&#xff0c;背景 二&#xff0c;华容道中道 1&#xff0c;问题 2&#xff0c;告诉脑机如何编码一个正方形格子 3&#xff0c;让脑机汇总信息 4&#xff0c;观察图&#xff0c;得到启发式算法 5&#xff0c;根据启发式算法求出具体解 6&#xff0c;可视化 一&am…

【Blockchain】GameFi | NFT

Blockchain GameFiGameFi顶级项目TheSandbox&#xff1a;Decentraland&#xff1a;Axie Infinity&#xff1a; NFTNFT是如何工作的同质化和非同质化区块链协议NFT铸币 GameFi GameFi是游戏和金融的组合&#xff0c;它涉及区块链游戏&#xff0c;对玩家提供经济激励&#xff0c…

python通过shapely 的 valid 判断aoi图形是否有效

测试aoi坐标&#xff1a; 116.527712,39.924304;116.527123,39.924353;116.52707,39.923985;116.527685,39.92397;116.527712,39.924304 如图所示是一个有效的坐标&#xff0c;使用python代码判断是否有效&#xff1a; 代码&#xff1a; from shapely.geometry import Polyg…

我开发了一款只用一个注解就实现分布式锁的工具框架

相信大家在JAVA中知道锁的一个概念。在JAVA中&#xff0c;锁是一种机制&#xff0c;用于控制并发代码的执行。锁用于保护共享资源的访问&#xff0c;确保只有一个线程能够同时访问这些资源。锁可以防止多个线程同时执行对共享资源的修改操作&#xff0c;从而避免数据不一致或竞…

探讨在大数据体系中API的通信机制与工作原理

** 引言 关联阅读博客文章&#xff1a;深入解析大数据体系中的ETL工作原理及常见组件 关联阅读博客文章&#xff1a;深入理解HDFS工作原理&#xff1a;大数据存储和容错性机制解析 ** 在当今数字化时代&#xff0c;数据已经成为企业发展和决策的核心。随着数据规模的不断增长…

zabbix_yum安装

目录 一.配置zabbix的yum源 二.安装zabbix server 三.安装zabbix agent 四.安装zabbix web界面 五.安装数据库 六.配置数据库 七.为zabbix server配置数据库 八.启动服务,web界面安装 九.遇到php版本过低问题 前置条件:基于Rocky Linux8操作系统配置的&#xff0c;建议…

Oracle19c ADG搭建

文章目录 一、环境配置1、主机环境2、host文件配置 二、主库配置1、 开启归档2、redo日志3、修改参数文件4、配置TNS文件5、静态监听6、拷贝密码文件 三、备库配置1、开启归档2、redo日志3、修改参数文件4、配置TNS文件5、配置静态监听 四、构建DG1、验证监听2、主库登入rman&a…

【计算机考研】408全年保姆级规划+资料分享

408的复习顺序其实没有标准&#xff0c;推荐先复习数据结构 复习完数据结构之后&#xff0c;再去学操作系统和计算机网络的一些知识点就会很好理解。 数据结构➡计算机组成原理➡操作系统➡计算机网络。 大家可以按照上面这个顺序来学&#xff0c;其实按照这个顺序来学也是因…

泛零售行业大会员经营的发展趋势?

​随着消费者需求的快速变化和技术的不断进步&#xff0c;泛零售行业大会员经营将呈现如下发展趋势: 第一&#xff0c;会员精细化运营和个性服务将上升为泛零售企业未来的战略重点之一。 存量时代&#xff0c;市场竞争加剧&#xff0c;对绝大多数泛零售企业来说&#xff0c;得…

2024三掌柜赠书活动第二十期:搜索之道:信息素养与终身学习的新引擎

目录 目录 前言 信息素养 终身学习 搜索引擎 信息素养与终身学习 关于《搜索之道&#xff1a;信息素养与终身学习的新引擎》 编辑推荐 内容简介 作者简介 图书目录 书中前言/序言 《搜索之道&#xff1a;信息素养与终身学习的新引擎》全书速览 结束语 前言 随着互…

密码算法概论

基本概念 什么是密码学&#xff1f; 简单来说&#xff0c;密码学就是研究编制密码和破译密码的技术科学 例题&#xff1a; 密码学的三个阶段 古代到1949年&#xff1a;具有艺术性的科学1949到1975年&#xff1a;IBM制定了加密标准DES1976至今&#xff1a;1976年开创了公钥密…

微服务之分布式事务概念

微服务之分布式事务概念 CAP定理和Base理论 CAP定理 CAP定理在1998年被加州大学的计算机科学家 Eric Brewer 提出&#xff0c;分布式系统有三个指标&#xff1a; 一致性&#xff08;Consistency&#xff09;可用性&#xff08;Availability&#xff09;分区容错性&#xff…

LLM应用:Prompt flow vs LangChain

背景 Prompt flow和LangChain都是LLM时代&#xff0c;为高效地构建LLM应用而生。 Prompt flow是Microsoft开源的&#xff0c;其诞生时&#xff0c;LangChain已经很有名气了。 所以作为后生的Prompt flow会为我们带来哪些新的东西呢&#xff1f; ​​​​​​​ Prompt flo…

一文了解JAVA的常用API

目录 常用kpimathSystemRuntimeObjectObjectsBigIntegerBigDecima正则表达式包装类 常用kpi 学习目的&#xff1a; 了解类名和类的作用养成查阅api文档的习惯 math 工具类。因为是工具类&#xff0c;因此直接通过类名.方法名(形参)即可直接调用 abs&#xff1a;获取参数绝对…

Docker容器与Serverless的融合:探索《2023腾讯云容器和函数计算技术实践精选集》中的云原生创新案例

Docker容器与Serverless的融合&#xff1a;探索《2023腾讯云容器和函数计算技术实践精选集》中的云原生创新案例 文章目录 Docker容器与Serverless的融合&#xff1a;探索《2023腾讯云容器和函数计算技术实践精选集》中的云原生创新案例一、引言二、《2023腾讯云容器和函数计算…

recover 的使用

一旦mayPanic触发了panic&#xff0c;控制流会跳到defer函数中&#xff0c;尝试执行recover。 如果recover捕获到了panic&#xff0c;它会阻止panic继续传播&#xff0c;程序控制流会继续在safeCall函数的defer函数之后进行。 然而&#xff0c;由于panic导致的提前返回&#xf…

Linux---多线程(下)

前情提要&#xff1a;Linux---多线程(上) 七、互斥 临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码&#xff0c;就叫做临界区互斥&#xff1a;任何时刻&#xff0c;互斥保证有且只有一个执行流进入临…