数据结构进阶(一):AVL树

news2024/11/24 1:24:23

所谓的AVL树也叫做高度平衡的二叉搜索树。

啥是高度平衡的二叉搜索树?

高度平衡的二叉搜索树:意味着左右子树的高度最大不超过一

我们先来回顾一下二叉搜索树的概念:

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

它或许是个完全二叉树:

在极端情况下又是个单分支的树:

一个二叉搜索树的时间复杂度为:O(N), 极端情况下,例如上图(左右单支)的情况,树的高度很高,那么就会导致搜索的效率很低,如果此时有N个树,树的高度就为N。

所以我们需要一颗更高效的树,这就是高度平衡的二叉搜索树,这也就是为什么需要高度平衡的原因。

AVL树的概念

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

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

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

如图:

 每个结点旁的数字代表着其平衡高度,左子树存在一个为 -1,右子树存在一个即为1,不存在则为0。

其中,3 结点上,左子树的高度为2,所以左子树的平衡因子应该为 -2,右子树的高度为1,所以右子树的平衡因子为1,二者相结合的平衡因子应该为:-1。

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

实现AVL树的相关代码

AVL树节点的定义

AVL树结点的定义其实很简单:

static class TreeNode {
    public TreeNode left; // 节点的左孩子
    public TreeNode right; // 节点的右孩子
    public TreeNode parent ; // 节点的双亲
    public int val = 0;
    public int bf = 0; // 当前节点的平衡因子=右子树高度-左子树的高度
    public TreeNode(int val) {
        this.val = val;
    }
}

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

AVL树的插入

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

  1. 按照二叉搜索树的方式插入新结点
  2. 调节结点的平衡因子

这里我们画图一步步来讲解。

我们先来随便画棵树:

现在我们有这样一棵AVL树,我们给它插入一个 5 的结点。

还是和二叉搜索树一样去寻找,从根节点开始,大于根结点的值向右走,小于根节点的向左走,以此类推。

此时,我们需要重新调节平衡因子:

此时就需要对整棵树进行一个旋转。

树的旋转

右单旋

 既然它不平衡,那就需要想办法让他平衡。

  这里只是其中一种旋转,我们再来看看其他情况:

左单旋

我们现在有这样一棵树:

 我们新插入一个50:

此时,25 的结点就不平衡了,我们也需要旋转:

旋转过后:

 由上述的两种旋转我们看到一个细节:

 

  

  • 不平衡时,该不平衡的结点和其子节点的平衡因子必然同号,此时我们只需要单旋即可。
  • 并且,平衡因子为负数需要发送右旋,平衡因子为正数需要发生左旋
  • 如若,异号则需要发生双旋

左右双旋

我们还是以发生右单旋的图为基础,我们现在不插入5,而是插入 28 ;

如图:

 我们可以看出来:30 这个节点上的平衡因子和 20 这个节点上的平衡因子异号了。

无论是左单旋还是右单旋都无济于事。不信可以自己去画画图;

最终结果如图所示:

 右左双旋

 我们在左单旋的基础上,插入一个值为 26 的结点,如下图:

同样的,无法用单旋来解决问题。

我们需要先右单旋在左单旋;如下图:

ok,上面只是介绍了单旋是怎么旋转的,接下来要开始讲解代码了。

代码实现

插入方法代码

首先,我们要将这个方法设置为布尔值类型,因为这个方法是有可能无法执行的。

AVL树是基于二叉搜索树衍生的,二叉搜索树中是无法实现插入相同的值。

所以这里的插入方法可以参考参考二叉搜索树。

第一步,需要判断根节点是否为空,为空那么这个需要插入的 val 就直接为根结点就行了。

第二步,开始遍历,设置 一个 cur ,一个 parent ,两个TreeNode ,去找目标 val 应该要插入的位置。

ok,目前为止与二叉搜索树没有太大差别,我们到这里已经找到了目标 val 应该插入的为止,还需要解决 平衡因子和转换后各个结点的关系。

此时的 node 已经是叶子节点了

为啥这里的循环条件是parent != null,我们来看张旋转图:

我们图中,30 是个根节点吗?

它也可以是某个结点的子节点(故此,我们还需要向上平衡);直到调节到根结点才算调整完毕。 

 

两两对比,就能总结出规律了。 

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;
            }
        }
        // cur == null
        if (parent.val < val) {
            parent.right = node;
        } else {
            parent.left = node;
        }

        // 当前只是解决了 left 和 right ,还没有处理 node 的 parent
        node.parent = parent;
        cur = node;

        // 调节平衡因子
        while (parent != null) {
            // 先看 cur 是 parent 的左还是右;此时决定了平衡因子是 ++ 还是 --
            if (cur == parent.right) {
                parent.bf++;
            } else{
                parent.bf--;
            }
            //检查当前平衡因子是否绝对值 <= 1
            if (parent.bf == 0) {
                // 记住一个结论: cur 的 parent 的平衡因子为 0 ,
                // 那么它插入一个结点一定平衡,此时无论插入右子树还是右子树都无所谓,就不用往上继续调节

                // 此时已经平衡了
                break;
            } else if (parent.bf == 1 || parent.bf == -1) {
                // 此时插入一个结点,其父节点未必平衡,需要继续向上调节
                cur = parent;
                parent = parent.parent;
            } else {
                if (parent.bf == 2) {
                    // 此时说明右树高,需要先左旋,再右旋
                    if (cur.bf == 1) {
                        rotateLeft(parent);
                    } else {
                        // cur.bf == -1
                        rotateRL(parent);
                    }
                } else {
                    // cur.bf == -2 ;左树高,需要降低左树的高度
                    // 同样也分平衡因子为 1 和 -1 两种情况
                    if (cur.bf == -1) {
                        // 右旋
                        rotateRight(parent);
                    } else {
                        // cur.bf == 1
                        rotateLR(parent);
                    }
                }
                break;
            }
        }
        return true;
    }

右单旋

还是需要借助上述的案例来进行讲解:

/**
     * 右单旋
     * @param parent
     */
    private void rotateLeft(TreeNode parent) {
        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;

        // 旋转关系 (需要画图)
        parent.right = subRL;
        subR.left = parent;
        if (subRL != null) { // subRL 可能为空
            subRL.parent = parent;
        }
        // 与左单旋一样,需要判断parent 所在的位置是根节点还是某个子树
        TreeNode pParent = parent.parent;
        parent.parent = subR;

        // 为根节点的情况
        if (root == parent) {
            root = subR;
            root.parent = null;
        } else {
            // 不为根节点的情况
            if (pParent.left == parent) {
                pParent.left = subR;
            } else {
                pParent.right = subR;
            }
            subR.parent = pParent;
        }
        // 调节平衡因子
        subR.bf = parent.bf = 0;
    }

这里的parent 是需要插入的叶子结点的父节点。

我们首先需要保存一下需要调整位置的几个结点:

第一步:先断开parent 和 subR 之间的关系,建立subRL 和 parent 之间的关系:

 

第二步:断开subR 和 subRL 之间的关系,建立subR 和 parent 之间的关系:

 

当然啦,subRL并非一定存在,它也可以不存在啊,不存在的情况需要特殊处理一下:

 

当然,我们目前只确定了彼此之间的left 和 right ,别忘了我们TreeNode 还定义了 parent,所以目前我们还需要处理一下父节点的问题。

 

如上图,我们需要确定parent 这个结点的位置是否为 根节点,如果是根结点那么 subR 的父节点就是null,如果不是则需要确定具体是 pParent 哪边:

ok,这里就是右单旋的代码,接下来看看左单旋;

左单旋

具体代码:

    /**
     * 左单旋
     * @param parent
     */
    private void rotateRight(TreeNode parent) {
        // 处理旋转之后几个节点的关系
        TreeNode subL = parent.left;
        TreeNode subLR = subL.right;
        parent.left = subLR;
        subL.right = parent;
        // subLR 是可能为空的,为空还进行就会报错!
        if (subLR != null) {
            subLR.parent = parent;
        }
        // 必须先记录父节点的父节点(这一段画图理解)
        TreeNode pParent = parent.parent;

        parent.parent = subL;
        // 判断 parent 是否为 根节点
        if(parent == root) {
            root = subL;
            root.parent = null;
        } else {
            // 不是根结点,就判断这棵树是左子树还是右子树
            if (pParent.left == parent) {
                pParent.left = subL;
            } else {
                pParent.right = subL;
            }
        }
        // 全部调整完毕,还需要调节 平衡因子
        subL.bf = 0;
        parent.bf = 0;
    }

同样的,我们得先保存几个需要调整位置的结点,如下图:

  我们先调整 几个结点之间的left 和 right 的关系;

第一步,断开parent 和 subL 之间的关系,连接 parent 和 subLR 的关系:

 第二步:断开subL 和 subLR 的关系,连接 subL 和 parent 的关系:

同样的,subLR并非一定存在,它也可以不存在啊,不存在的情况需要特殊处理一下:

 

 

接下来的操作都一样:

 

 

右左双旋

确实双旋比单旋难不了多少,双旋是建立在单旋的基础上的,只需要单旋两次即可。

先看示意图:

 

    /**
     * 右左双旋
     * @param parent
     */
    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 {
            // bf == 1
            subL.bf = -1;
            subLR.bf = 0;
            parent.bf = 0;
        }
    }

 我们来看看这两种情况:

于是就变成了这样:

 

此时的subLR 的平衡因子就是 -1 了,所以需要保留一下sunLR的平衡因子;

旋转还是一样的旋转,只是旋转以后需要改变不同的平衡因子:

 

同样的, 左右双旋也是同理。

这里就不单独介绍左右双旋,可以自己画个图,然后参考上面右左双旋的做法。

此外还有一个删除没有了解,过后我在此处补齐。

完整代码:

AVLTree/src/AVLTree.java · wjm的码云/Projects - 码云 - 开源中国 (gitee.com)

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

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

相关文章

Git 分支详解

一、分支概念介绍 1. 主分支&#xff08;主干/主线/main/master&#xff09; 包含所有最终修改的历史&#xff0c;反映项目的最终版本。 建议不要乱动主干&#xff0c;如果你编辑了一个小组项目的主干分支&#xff0c;你的改动会影响到其他人&#xff0c;而且很快就会出现合并…

jmeter负载测试如何找到最大并发用户数

在性能测试中&#xff0c;当我们接到项目任务时&#xff0c;很多时候我们是不知道待测接口能支持多少并发用户数的。此时&#xff0c;需要我们先做负载测试&#xff0c;通过逐步加压&#xff0c;来找到最大并发用户数。那么当我们找到一个区间&#xff0c;怎么找到具体的值呢&a…

实验室仪器管理系统/基于微信小程序的实验室仪器管理系统

摘 要 随着当今网络的发展&#xff0c;时代的进步&#xff0c;各行各业也在发生着变化&#xff0c;于是网络已经逐步进入人们的生活&#xff0c;给我们生活或者工作提供了新的方向新的可能。 本毕业设计的内容是设计实现一个实验室仪器管理系统。使用微信开发者是以java语言…

<Java导出Excel> 1.0 Java实现Excel动态模板导出

思路&#xff1a; 1&#xff0c;先创建动态模板&#xff08;必须要在数据库建一张表&#xff0c;可随时修改模板&#xff09; 例如&#xff1a; 建表语句&#xff1a; CREATE TABLE list_table_header (headerName VARCHAR(100) NOT NULL,headerField VARCHAR(100) NOT NULL…

一起了解抖音共创功能:激发创意,合作共创更有趣的短视频

抖音共创功能是抖音短视频平台推出的一项创作工具&#xff0c;旨在鼓励用户通过合作共创来创作更有趣、创意的短视频内容。该功能于2020年11月上线&#xff0c;受到了广大用户的热烈欢迎。下面不若与众科技就来介绍一下抖音共创功能。 抖音共创功能允许用户邀请其他用户一同参与…

Neo4j图数据库的使用笔记

Neo4j图数据库的使用笔记 win系统安装Neo4j图数据库 安装准备&#xff1a; neo4j-3.4.0版本的zip包 找个目录解压安装zip包 启动neo4j 下载neo4j-3.4.0版本的zip包 可以去neo4j官网下载&#xff0c;也可以去微云数聚官网下载。 微云数聚是neo4j在国内的代理商。 解压到…

sql分组查询

多个相同去重 思路&#xff1a; 找where条件 分组 分组后过滤

java导入csv格式文件之身份证格式处理

一. 出现的问题 csv中的身份证号如下图&#xff1a; 导到数据库中的结果 因此怎样导入才能使身份证能够正常导入呢&#xff1f; 2. 解决方案 第一步&#xff1a; 选中身份证那一列 第二步&#xff1a; 右键选择&#xff0c;设置单元格格式 第三步&#xff1a; 数字列中&#x…

面试必问之锁的底层原理

sychrionzed的底层原理: 实例数据&#xff1a;存放类的属性数据信息&#xff0c;包括父类的属性信息&#xff1b; 对齐填充&#xff1a;由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的&#xff0c;仅仅是为了字节对齐&#xff1b;根据寻址优化算法 s…

开源免费多语言翻译模型

今天给大家介绍赫尔辛基大学开源免费的多语言翻译模型&#xff0c;赫尔辛基大学开发了1400多个多语种翻译模型&#xff0c;我们可以在Hugging Face网站上免费下载免费使用这些模型,今天我来介绍其中的中译英和英译中两个模型。 我机器的环境是win11,adaconda&#xff0c;pytho…

C++笔记之是否知道了一个数组首元素的地址就可以获取该整个数组?-合众新能源汽车面试官问题

C笔记之数组名和指针GPT问答记录 事前提要&#xff1a;前段时间去面合众新能源汽车&#xff0c;面试官说知道了一个数组首元素的地址&#xff0c;就可以获取该整个数组&#xff0c;我当时就困惑&#xff0c;不知道大小和长度&#xff0c;只知道地址怎么就能获取。但也没反驳面…

windows,win10去掉命令行窗口cmd选中内容后导致中断的问题

背景 cmd黑窗口被选中内容后&#xff0c;会暂时挂起/阻塞进程。如&#xff1a;node.js的一些命令&#xff0c;选中刷出的日志会导致请求阻塞、http请求后端api接口的日志被选中后也会阻塞 方法 左键点击cmd黑窗口左上角的图标&#xff0c;选择 “属性”&#xff08;要注意选…

9.3.1 【Linux】区块选择(Visual Block)

当我们按下 v 或者 V 或者 [Ctrl]v 时&#xff0c;这个时候光标移动过的地方就会开始反白&#xff0c;这三个按键的意义分别是&#xff1a; 9.3.2 多文件编辑 我们可以使用vim后面同时接好几个文件来同时打开&#xff0c;相关的按键有&#xff1a; 9.3.3 多窗口功能 分区窗口…

高压放大器在半导体测试行业的应用

半导体测试是一项非常重要的工作&#xff0c;它涉及到对半导体芯片进行电气、物理等方面的测试&#xff0c;以确保产品质量和性能。测试过程中需要使用高精度、高可靠性的测试设备和仪器&#xff0c;而高压放大器则是其中的一种重要工具。 在半导体测试中&#xff0c;高压放大器…

【ARM】-异常中断处理概述

文章目录 控制程序执行流程ARM 体系中异常中断种类 控制程序执行流程 在 ARM 体系中通常有以下 3 种方式控制程序的执行流程&#xff1a; 在正常程序中执行过程中&#xff0c;每执行一条 ARM 指令&#xff0c;程序计数寄存器 PC 的值加 4 个字节&#xff1b;每执行一条 Thumb…

Java——异常学习

一、什么是异常 Java的异常是指在程序运行过程中可能发生的错误或异常情况,如数组越界、除零错误、空指针等。 备注&#xff1a;(开发过程中的语法错误和逻辑错误不是异常) 二、Java异常分类 三、异常处理方式 下面先看以下代码 //下面创建一个用于计算的类 class calculate…

OpenCV读取一张深度图像并显示

#include <iostream> #include <opencv2/imgcodecs.hpp> #include <opencv2/opencv.hpp> #include

openGauss学习笔记-01 什么是openGauss

文章目录 openGauss学习笔记-01 什么是openGauss1.1 openGauss是一个数据库管理系统1.2 openGauss数据库是关系型的1.3 openGauss软件是开源的1.4 openGauss数据库具有高性能、高可用、高安全、易运维、全开放的特点1.4.1 高性能1.4.2 高可用1.4.3 高安全1.4.4 易运维1.4.5 全开…

Nanopc T4 教程使用及获取

所有资料从以下链接获取 ● Wiki教程&#xff1a;http://wiki.friendlyarm.com/wiki/index.php/NanoPC-T4/zh 机械结构图&#xff08;dxf格式&#xff09;&#xff1a;http://wiki.friendlyarm.com/wiki/images/b/bc/NanoPC-T4_1802_Drawing%28dxf%29.zip 原理图&#xff1a;h…

一百二十七、海豚调度——dolphinscheduler205单机版安装

一、前提准备 1.JDK1.8 2.MySQL&#xff08;5.7&#xff09; 3.ZooKeeper&#xff08;3.4.6&#xff09; 4.需安装进程树分析工具 psmisc [rootlinux128 ~]# sudo yum install -y psmisc 5.海豚调度安装包和MySQL驱动包 apache-dolphinscheduler-2.0.5-b…