高阶数据结构之AVL树

news2024/9/27 9:22:04

文章目录

  • 回顾二叉搜索树
  • AVL树
    • 在AVL树中插入新节点
    • AVL树中的各种旋转
      • 右单旋
      • 左单旋
      • 左右双旋
      • 右左双旋
    • 验证是否是AVL树
      • 验证是否是二叉搜索树
      • 验证是否是平衡树
  • 总结AVL树

回顾二叉搜索树

        二叉搜索树的一些特点回顾:
        (1)每一个节点左树上所有节点的值都是小于当前节点的值;每一个节点右树上的所有节点的值都是大于当前节点的值。
        (2)对二叉搜索树进行层序遍历的时候,输出的是有序的序列,。
        (3)二叉搜索树,顾名思义就是用来进行搜索的,当这棵二叉搜索树是平衡的时候,查询效率是最高的,但是当节点是有序插入二叉搜索树中的时候,这棵二叉搜索树就会退化成一个链表,此时的查询效率是很低的。所以总的来说,二叉搜索树的搜索效率取决于树的高度。


        本文是通过上面二叉搜索树中特性三进行展开的,由于二叉搜索树可能会存在树的高度不平衡,从而导致查询效率低下的情况,对此就引出了一种比较好的解决方案 — 本文主要的内容就是约束这棵树的左右树高度差不能超过1(当然也有其他的约束规则, 后面文章会再说到),通过这个约束来尽可能地将树的高度控制平衡,进而提高查询效率,而这就是我们这篇文章中所要总结的AVL树。

AVL树

        AVL树的特点是:树上的每一个节点在定义的时候都会维护一个平衡因子,而这个平衡因子其实就是当前节点右子树的高度-当前节点左子树的高度。并保持这个平衡因子的绝对值不大于1,当大于1的时候则会发生旋转来保持树高度的平衡(如何旋转会在下面总结)。

定义AVL树的代码:

static class TreeNode{
    public int val;
    public TreeNode left;
    public TreeNode right;
    public TreeNode parent;
    public int bf;  //平衡因子

    public TreeNode(int val){
        this.val = val;
    }
}

在AVL树中插入新节点

        AVL树的插入主要分为两个步骤:
        1.按照二叉搜索树的规则来将节点插入到AVL树中。具体就是判断要插入的数是否大于当前节点的值,如果大于的话就继续往右子树上遍历,如果小于的话就继续往左子树上遍历,如果等于的话就说明是非法插入……直到遍历到叶子节点处插入即可。
        2.当插入新节点后,并且AVL树的平衡遭到破坏的时候,就需要更新平衡因子,以保证AVL树的平衡性。具体的逻辑是:
                (1)修改平衡因子(从插入节点一直往上进行修改,如果当前节点在parent节点的左侧,则对parent节点的平衡因子-1;如果当前节点在parent节点的右侧,则对parent节点的平衡因子+1)。
                (2)当每次修改完parent的平衡因子后,其平衡因子会出现三种情况:
                        一、如果平衡因子是0,那么说明在插入之前parent节点的平衡因子是±1,插入之后才可能变成0,那么进一步说明此时的AVL树已经平衡,停止继续往上调整;
                        二、如果平衡因子是±1,那么说明在插入之前parent节点的平衡因子是0,进一步说明AVL树的高度增加,需要继续向上调整;
                        三、如果平衡因子是±2,此情况只会在向上调整的过程中出现,一旦遇到这种情况,那么就说明parent节点上的平衡因子违反了AVL树平衡的性质,那么就需要对树进行旋转操作,具体如何旋转请继续往下看。

public boolean insert(int val){
    TreeNode node = new TreeNode(val);
    if(root == null){
        root = node;
        return true;
    }
    TreeNode parent = null;
    TreeNode cur = root;
    while(cur != null){
        if(cur.val < val){
            parent = cur;
            cur = cur.right;
        }else if(cur.val == val){
            return false;
        }else{
            parent = cur;
            cur = cur.left;
        }
    }

	//插入到叶子节点上
    if(parent.val < val){
        parent.right = node;
    }else{
        parent.left = node;
    }
    node.parent = parent;
    cur = node;

    //修改平衡因子
    while(parent != null){
        if(cur == parent.right){
            parent.bf++;
        }else{
            parent.bf--;
        }
        //确定是否需要继续向上调整
        if(parent.bf == 0){
            //当出现这种情况说明已经平衡
            break;
        }else if(parent.bf == 1 || parent.bf == -1){
            //当出现这种情况需要继续向上判断
            cur = parent;
            parent = cur.parent;
        }else{
            if(parent.bf == 2){

            }else{
                //相当于parent.bf == -2
                
            }
        }
    }
}

AVL树中的各种旋转

        如果一颗树原来是平衡的AVL树,当插入一个新的节点并导致树不平衡的时候,就需要对树的结构进行调整,可以通过旋转的方式来让树变得平衡,具体有以下四种旋转方式(右单旋、左单旋、左右双旋、右左双旋):

右单旋

        右旋的本质其实就是降低左树的高度,下面用一个图来进行解释:


我们假设一开始的AVL树是这样子的(所有节点的平衡因子的绝对值都不大于1,符合):
在这里插入图片描述
当往这个AVL树中插入值为10的节点后(向上调整平衡因子后会发现在值为60的节点上的平衡因子为-2,违反了AVL树平衡的性质):
在这里插入图片描述
通过右单旋操作之后就可以让AVL树恢复平衡(由于是左树高,需要降低左树的高度就必须使用右旋解决):
在这里插入图片描述
由上述的这个旋转规则我们发现我们只需要改变两处指向以及两处平衡因子就可以让AVL树恢复平衡,那么我们就可以使用代码来实现:

private void rotateRight(TreeNode parent) {
    TreeNode subL = parent.left;
    TreeNode subLR = subL.right;
    TreeNode pParent = parent.parent;

    //修改指向
    parent.left = subLR;
    subL.right = parent;
    if(subLR != null) {
        subLR.parent = parent;
    }
    parent.parent = subL;
    if(parent == root){
        root = subL;
        subL.parent = null;
    }else{
        if(pParent.left == parent){
            pParent.left = subL;
            subL.parent = pParent;
        }else{
            pParent.right = subL;
            subL.parent = pParent;
        }
    }

    //调整平衡因子
    subL.bf = 0;
    parent.bf = 0;
}

左单旋

        左旋的本质其实就是降低右树的高度,其实左单旋与上面的右单旋是非常类似的,基本上就只是上面右单旋图的镜像图,此处不再详细画出旋转图。

代码实现:

private void rotateLeft(TreeNode parent) {
    TreeNode subR = parent.right;
    TreeNode subRL = subR.left;
    TreeNode pParent = parent.parent;

    //修改指向
    parent.right = subRL;
    subR.left = parent;
    if(subRL != null){
        subRL.parent = parent;
    }
    parent.parent = subR;
    if(parent == root){
        root = subR;
        subR.parent = null;
    }else{
        if(pParent.left == parent){
            pParent.left = subR;
            subR.parent = pParent;
        }else{
            pParent.right = subR;
            subR.parent = pParent;
        }
    }

    //调整平衡因子
    subR.bf = 0;
    parent.bf = 0;
}

左右双旋

我们假设一开始的AVL树是这样子的(所有节点的平衡因子的绝对值都不大于1,符合):
在这里插入图片描述
当往这个AVL树中插入值为35的节点后(向上调整平衡因子后会发现在值为60的节点上的平衡因子为-2,违反了AVL树平衡的性质):
在这里插入图片描述
通过左右双旋操作之后就可以让AVL树恢复平衡(因为我们无论是只进行左单旋或者是只进行右单旋,都无法使得这棵树平衡):
在这里插入图片描述
此外,还有另外一种是插入节点值为45的情况,我们也依旧可以使用左右双旋完成平衡:
在这里插入图片描述
综上两种情况可以确定规律:
1.当出现 parent 的平衡因子是 -2 且 parent.left 是 1 的时候,都是可以进行左右双旋来达到AVL树的平衡。
2.当插入节点后 parent.left.right 的平衡因子是 -1 的时候,就会构成 0-0-1 的形式;当插入节点后 parent.left.right 的平衡因子是 1 的时候,就会构成 -1-0-0 的形式。


由此两个规律以及旋转示意图,我们就可以使用代码来实现:

private void rotateLR(TreeNode parent) {
    TreeNode subL = parent.left;
    TreeNode subLR = subL.right;
    int bf = subLR.bf;

    //先左旋
    rotateLeft(parent.left);
    //再右旋
    rotateRight(parent);

    //调整平衡因子
    if(bf == -1){
        subL.bf = 0;
        subLR.bf = 0;
        parent.bf = 1;
    }else if(bf == 1){
        subL.bf = -1;
        subLR.bf = 0;
        parent.bf = 0;
    }
}

右左双旋

        右左双旋的本质其实左右双旋是非常类似的,基本上就只是上面左右双旋图的镜像图,具体的操作步骤也是镜像的,此处不再详细画出旋转图。

代码实现:

private void rotateRL(TreeNode parent) {
    TreeNode subR = parent.right;
    TreeNode subRL = subR.left;
    int bf = subRL.bf;

    //先右旋
    rotateRight(parent.right);
    //再左旋
    rotateLeft(parent);

    //调整平衡因子
    if(bf == 1){
        parent.bf = -1;
        subR.bf = 0;
        subRL.bf = 0;
    }else if(bf == -1){
        parent.bf = 0;
        subR.bf = 0;
        subRL.bf = 1;
    }
}

验证是否是AVL树

        我们前面说过,AVL树其实就是一颗高度平衡的二叉搜索树,所以我们在对AVL树进行验证的时候,可以分为两步来进行验证:验证这棵树是否是二叉搜索树 & 验证这棵树是否是平衡的。

验证是否是二叉搜索树

        我们在验证一棵树是否是二叉搜索树的时候,一般都是使用中序遍历来查看遍历的结果是否是有序的。

//中序遍历判断结果是否有序
public void inorder(TreeNode root){
    if(root == null) return;
    inorder(root.left);
    System.out.println(root.val + " ");
    inorder(root.right);
}

验证是否是平衡树

        我们在判断AVL树是否平衡不能够直接判断所有节点上的平衡因子的绝对值是否小于等于1,原因是有可能我们前面计算出来的平衡因子就是错误的,正确的验证步骤应该是遍历判断节点的左右子树的高度差小于等于1。

//获取树的高度
public int height(TreeNode root){
    if(root == null) return 0;
    int leftH = height(root.left);
    int rightH = height(root.right);
    return leftH > rightH ? leftH + 1 : rightH + 1;
}

//判断树是否平衡
public boolean isBalanced(TreeNode root){
    if(root == null) return true;
    int leftH = height(root.left);
    int rightH = height(root.right);
    if(rightH - leftH != root.bf){
        System.out.println("节点:" + root.val + "平衡因子异常");
        return false;
    }
    return Math.abs(leftH - rightH) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}

总结AVL树

        AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效(logN)的时间复杂度。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合,这时候就可以考虑一下使用红黑树(下一章的内容)。
        当然,说道AVL树节点的删除操作,这种情况几乎不会使用到(原因如上所述),但是这里总结一下删除操作的基本思路:
        1.找到需要删除的节点;
        2.按照二叉搜索树的删除规则删除这个节点;
        3.每次更新一下平衡因子,如果出现不平衡的情况,则需要采用旋转(左单旋、右单旋、左右双旋、右左双旋)来进行调节(注意也是需要画图分析)。
        最后附上AVL树的完整代码,有需要可以访问此gitee账户(链接)进行参考:AVL树代码。

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

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

相关文章

vue多实例的骚操作,主要用于解决组件全局弹窗面板的问题。。。

1.问题背景 主要是自己写了一个组件库&#xff0c;其中涉及到弹出面板的组件遇到兼容性问题。 举个例子&#xff0c; 日期选择组件例如 DaterPicker组件 大概的代码如下&#xff08;省略了细节实现&#xff09; <template> <label>日期</label> <input …

一文详解PHP用流方式实现下载文件(附代码示例)

一淘模板给大家带来了关于PHP的相关知识&#xff0c;其中主要介绍了在PHP中怎么使用流方式来实现下载文件的&#xff0c;下面一起来看一下&#xff0c;希望对大家有帮助。 PHP 中使用流方式下载文件 在 PHP 中&#xff0c;可以使用 fopen() 函数打开一个远程文件&#xff0c;并…

Unity使用本地UPM包的实现方式

实现1&#xff1a;项目根路径实现优点&#xff1a;1.不必有额外操作2.本地包随项目版控&#xff0c;不会丢失包产生错误3.按需升级包缺点&#xff1a;1.包的修改随项目版控&#xff0c;增加日志冗余2.不利于包全局管理建议使用场景多人合作&#xff0c;开发底子较弱的团队。实现…

今年春节,全国物流很稳!

我叫张雄伟&#xff0c;是腾讯安全的一位交付工程师。我和团队的主要工作&#xff0c;是物流行业重点客户的安全防御项目管理工作。近两年&#xff0c;我们开始向顺丰、极兔等多家大型物流公司提供安全产品与服务。在双十一、618等全民购物节、春节等电商旺季&#xff0c;我们和…

百华鞋业开工大吉|起航新征程,扬帆再出发

爆竹声声迎鸿运&#xff0c;开工大吉启新程。2023年农历正月初六&#xff0c;山东百华鞋业有限公司迎来节后开工吉日。百华的家人们迅速集结工作岗位&#xff0c;以饱满的精神状态&#xff0c;按下工作复位键&#xff0c;俯身蹬地冲起跑&#xff0c;努力奋斗再出发&#xff01;…

史上最全测试开发工具推荐(含自动化、性能、稳定性、抓包)

一、UI自动化测试工具 1. uiautomator2 介绍: openatx开源的ui自动化工具&#xff0c;支持android和ios。主要面向的编程语言是python&#xff0c;api设计简洁易用&#xff0c;在开源社区也是很受欢迎。 原理图&#xff1a; 安装&#xff1a; pip install --upgrade --pre…

【C语言】-扫雷-简单版

前言&#xff1a;感谢各位朋友的捧场&#xff0c;这里给大家分享的是扫雷游戏的简单实现 &#xff08;PS&#xff1a;这里简单是指只实现了游戏的基础功能和主要流程&#xff0c;由于当前本人技术知识尚薄弱&#xff0c;相关的优化会通过后续的学习进行更新&#xff09; 《扫雷…

session,cookie和token的区别

session&#xff0c;cookie和token究竟是什么简述cookie&#xff0c;session&#xff0c;token作为面试必问题&#xff0c;很多同学能答个大概&#xff0c;但是又迷糊不清&#xff0c;希望本篇文章对大家有所帮助http是一个无状态协议什么是无状态呢&#xff1f;就是说这一次请…

【Java Swing】Java组件及事件处理

图形用户接口1、Swing概述2、Swing顶级容器3、布局管理器4、事件处理5、Swing常用组件1、Swing概述 Swing是一种轻量级的组件&#xff0c;它由Java语言开发&#xff0c;可以通过使用简洁的代码、灵活的功能和模块化的组件来创建优雅的用户界面Swing组建的继承关系 2、Swing顶…

企业为何都用电子招投标 现代电子招投标系统介绍

在以前的传统招投标工作中&#xff0c;主要采用人工、书面文件的模式操作&#xff0c;往往产品没有得到很好地分类&#xff0c;导致整个招投标流程变得漫长且复杂。在传统招投标过程中通常需要三个月或更长时间&#xff0c;这对于买方或供应商企业而言是非常浪费时间的。如果还…

LIO-SAM代码解析——imageProjection.cpp

目录imageProjection.cpp1. ImageProjection类1.1. imuHandler1.2. odometryHandler1.3. cloudHandler⭐1.3.1. cachePointCloud&#xff1a; 点云消息缓存与检查1.3.2. deskewInfo() &#xff1a; 获得运动补偿信息1.3.2.1. imuDeskewInfo() &#xff1a; imu的补偿信息1.3.2.…

TOOM系统加强网络舆情监控的建议,如何加强网络舆情的引导和管控

网络舆情监控是指在互联网上通过技术手段&#xff0c;对网络上的舆情信息进行收集、整理、分析、评估和处理&#xff0c;以有效地识别、预测、处理网络舆情问题。网络舆情监控工作的目的是促进舆情健康&#xff0c;防止舆情危机。接下来简单了解TOOM系统加强网络舆情监控的建议…

Python基本语法与变量类型

一、Python基本语法 1、Python注释 Python 支持两种类型的注释&#xff0c;分别是单行注释和多行注释。 &#xff08;1&#xff09;单行注释 单行注释指的是从井号#开始&#xff0c;直到这行结束为止的所有内容都是注释。 # 注释内容&#xff08;2&#xff09;多行注释 Pyt…

ccflow-代码

报表设计目录概述需求&#xff1a;设计思路实现思路分析报表设计&#xff0c;流程运维系统&#xff08;三元log&#xff09;数据源管理和维护是否:debug状态. 0 表示不是, 1 是&#xff0c;如果系统发布后&#xff0c;请将此修改成0&#xff0c;以提高执行效率。在流程运行结束…

Python语言开发学习之使用Python预测天气

什么是wttr&#xff1f; 使用Python预测天气的第一步&#xff0c;我们要了解wttr是什么。wttr.in是一个面向控制台的天气预报服务&#xff0c;它支持各种信息表示方法&#xff0c;如面向终端的ANSI序列(用于控制台HTTP客户端(curl、httpie或wget))、HTML(用于web浏览器)或PNG(…

【微信小程序】解决点击(bindtap)和长按(bindlongtap)冲突

点击事件的执行&#xff1a; <button bindtap"bindtap" bindtouchstart"touchstart" bindtouchend"touchend">按钮</button>可以看到顺序为&#xff1a;touchstart → touchend → tap 长按事件的执行&#xff1a; <button bin…

Blender 渲染与后期处理

文章目录旋转环境贴图&#xff08;天空盒&#xff09;物体只渲染其他物体的阴影而不渲染自身渲染一个背景透明的图片在后期合成中&#xff0c;将渲染结果和一张图片合成到一起输出不同的通道方法一方法二后期制作景深效果渲染单个图层图层渲染单个图层旋转环境贴图&#xff08;…

对程序员超有用的网站!一定要收藏起来!

作为一名专业的程序员&#xff0c;我们应该利用各种渠道来扩充自己的知识。然后做一个技术高超的打工人&#xff01;&#xff08;&#xff09;然后用自己超高的技术&#xff0c;赚超多超多的money! (√) 但是要获取大量的信息就要有优质可靠的信息来源。今天我就把我珍藏的&…

CMOS图像传感器——深入ISO

在之前讲Dual Gain这一HDR技术时,有大致提到过ISO: HDR 成像技术学习(二)_沧海一升的博客-CSDN博客HDR成像技术介绍:staggered HDR、DOL-HDR、DCG,双原生ISO等。https://blog.csdn.net/qq_21842097/article/details/120904447 这一篇文章我们深入讲解一下。 通常…

IP协议详解

IP协议 IP协议格式&#xff1a; 4位版本号&#xff1a;指定IP协议的版本&#xff0c;对于IPv4来说&#xff0c;就是4。 4位首部长度&#xff1a;IP头部的长度是多少个32bit(4字节)&#xff0c;也就是 length * 4 的字节数。4bit表示最大的数字是15&#xff0c;因此IP头部最大…