手撕AVL_二叉平衡树(图文并茂)

news2024/11/18 4:48:43

目录

前言

一 . AVL树的概念

二 . AVL树节点的定义

三 . AVL树的插入

1.插入节点

2.调节负载因子

四 . AVL树的旋转

1.左单旋

2.左右双旋

五 . AVL树性能分析

总结


前言

大家好,今天带大加手撕AVL树的插入


一 . AVL树的概念

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

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

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


二 . AVL树节点的定义

为了AVL树实现简单,AVL树节点在定义时维护一个平衡因子,具体节点定义如下:

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

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


三 . AVL树的插入

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

1.二分查找树的形式进行插入
while(){
      1.1 当前节点的val小于待插入节点的val -> 往右边迭代
      1.2 当前节点的val大于待插入节点的val -> 往左边迭代
      1.3 当前节点的val等于待插入节点的val -> 不允许插入
    }
      1.4 正式插入节点

2.调节平衡因子
    2.1 如果当前节点是父节点的左节点 parent.bf++
    2.2 如果当前节点是父节点的右节点 parent.bf--;
    2.3 根据parent.bf判断是否需要继续向上调整
        2.3.1 如果parent.bf == 0 表示当前树平衡
        2.3.2 如果parent.bf == (1 || -1) 当前子树平衡,子树上面的情况未知,主要向上进行调整
        2.3.3 如果parent.bf == (2 || -2) 需要进行旋转,根据不同的情况,采取不同的旋转策略

1.插入节点

本来还想偷个懒的,发现二叉搜索树没有写,这就没办法了,一步一步来吧

假如我们需要在下面avl树中插入节点10

parent: 表示当前遍历到的节点的父节点 初始值为null

child: 表示当前遍历到的节点 初始值为root 即 5号节点

1.4 正式插入节点

给出部分代码

        TreeNode parent = null;
        TreeNode child = root;

        // 1.二分查找树的插入
        while(child != null){
            if(child.val > val){ //
                // 往左边插入
                parent = child;
                child = child.left;
            }else if(child.val < val){
                // 往右边插入
                parent = child;
                child= child.right;
            }else{
                // 树中已经存在该节点,不允许插入
                return false;
            }
        }

        // 节点正式插入
        if(parent.val > val){
            parent.left = node;
        }else{
            parent.right = node;
        }
        node.parent = parent;
        child = node;

至此第一步完美结束.我们开始第二大步调节平衡因子

2.调节负载因子

负载因子的调节和二叉树的旋转可以说是平衡树的核心了,但是只要理解透了,就是随便写,毫无难度!

  • 2.1 如果当前节点是父节点的左节点 parent.bf++
  • 2.2 如果当前节点是父节点的右节点 parent.bf--;

此时如果我们插入的元素使10,那parent.bf == 1

2.3.2 如果parent.bf == (1 || -1) 当前子树平衡,子树上面的情况未知,需要向上进行调整

以9为根节点的子树确实是平衡了,但是上面的子树我们并不清楚,可能平衡也可能不平衡,上面的两个都是不平衡的,我们来看一个平衡的例子

上面的意思是当前子树平衡,子树上面的情况未知,需要向上进行调整我们只知道以parent为根节点的子树的情况,并不清楚上面的情况

再次回到上面插入10的例子中,根据2.3.2可知需要向上调整

2.3.3 如果parent.bf == (2 || -2) 需要进行旋转,根据不同的情况,采取不同的旋转策略

到这就是开始旋转了

旋转一共分为四种,表示四种不同的情况,我先把这四种旋转应用的情况列举出来,具体如何旋转等到下面会细说

1.左单旋 

2.右单旋

3.左右双旋

4.右左双旋

这四种旋转就是AVL树最核心的地方了,相比大家也看出来了,在神魔情况下用哪一种旋转,我都用蓝圈圈起来了,这里来总结一下

1.如果 parent.bfchild.bf 都是正数,那么应该进行左单旋(同正左旋)

2.如果 parent.bfchild.bf 都是负数,那么应该进行右单旋(同异右旋)

3.如果 parent.bf 为正 child.bf为负 ,右左双旋

4.如果parent.bf为负  child.bf 为正 ,左右双旋

记的话估计不太好记,但是有一点一定可以记住,同单异双(同号单旋,异号双旋),大家还是重在理解


给出部分代码

        // 2.调节平衡因子
        while(parent != null){
            // 先看child是parent的左还是右,判断bf是++还是--
            if(child == parent.right){
                // child是parent的左树 bf++
                parent.bf++;
            }else{
                // child是parent的右树 bf++
                parent.bf--;
            }

            // 根据parent的负载因子判断是否需要继续向上调整 bf[-1,0,1]
            if(parent.bf == 0){
                // 当前子树平衡代表上面的也已经平衡
                break;
            }else if(parent.bf == 1 || parent.bf == -1){
                // 上面的树不一定平衡,继续向上调整
                child = parent;
                parent = parent.parent;
            }else {
                if (parent.bf == 2) {
                    if (child.bf == 1) {
                        // 左单旋
                        rotateLeft(parent);

                    } else if (child.bf == -1) {
                        // 右左双旋
                        rotateRL(parent);
                    }
                } else {
                    if (child.bf == -1) {
                        // 右单旋
                        rotateRight(parent);
                    } else if (child.bf == 1) {
                        // 左右双旋
                        rotateLR(parent);
                    }
                }
                // 上述代码走完平衡!!
                break;
            }
        }

下面就是真正写旋转的代码了,我会带着大家实现一个单旋和一个双旋,剩下的就是照着葫芦画瓢,相比大家应该没什么问题


四 . AVL树的旋转

1.左单旋

我们先定义变量,不需要为了节省空间搞成parent.parent.left.right.left 举个例子,实际上没有这么多嵌套

现在来想一个问题: 如果60往上提,那以40为根节点的子树该放在哪? 想清楚这个问题,那么恭喜你

你完全有能力去手撕后面的代码,即使一些细节你考虑不到! 不要往下看,自己想想吧,下面的图会有助于大家理解的

首先要明白一个问题,60提上去之后成为了这颗子树的根节点,根据二叉搜索树的性质,40应该放在60的左侧,答案依然明了,30的左子树指向60的左子树

到这里已经可以手撕一部分代码了

以为到这里就忘了吗? nonono 在这里发出灵魂三问

1. 30是否还有父节点? 如果有的话不改变指向的话会怎么样?

2.subRL有没有可能为空? 如果为空会不会发生空指针异常?

3.parent有没有可能为root?

都是一些细节问题,直接给出代码,大家自行理解,理解不了评论区留言

private void rotateLeft(TreeNode parent) {
        // 定义变量
        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;

        // 保存parent的父节点 问题1
        TreeNode pParent = parent.parent;
        
        // 问题2 增加空指针判断
        if(subRL != null) subRL.parent = parent;
        parent.right = subRL;
        subR.left = parent;
        parent.parent = subR;

        // 问题3 增加判断
        if(parent == root){
            root = subR;
            root.parent = null;
        }else{
            if(pParent.left == parent){
                pParent.left = subR;
            }else{
                pParent.right = subR;
            }
            subR.parent = pParent;
        }
        // 根据图示修改平衡因子
        parent.bf = subR.bf = 0;
    }

右旋

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

        TreeNode pParent = parent.parent;
        if(subLR != null) subLR.parent = parent;
        parent.parent = subL;
        parent.left = subLR;
        subL.right = parent;

        if(parent == root){
            // 当前的parent是根节点
            root = subL;
            root.parent = null;
        }else{
            if(pParent.left == parent){
                pParent.left = subL;
            }else{
                pParent.right = subL;
            }
            subL.parent = pParent;
        }
        parent.bf = subL.bf = 0;
    }

2.左右双旋

左右双旋无非就是经过两次旋转,只要单旋搞清楚了,双旋完全没有难度,值的一提的是,平衡因子的修改需要根据subLR.bf来进行判断,除此之外,没有其他的细节了

    /*
    * 右左双旋
    * */
    private void rotateRL(TreeNode parent) {
        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;
        int bf = subRL.bf;

        rotateRight(subR);
        rotateLeft(parent);

        if(bf == -1){
            parent.bf = subRL.bf = 0;
            subR.bf = 1;
        }else{
            subRL.bf= subR.bf = 0;
            parent .bf = -1;
        }
    }

    /*
    * 左右双旋
    * */
    private void rotateLR(TreeNode parent) {
        TreeNode subL = parent.left;
        TreeNode subLR = subL.right;

        int bf = subLR.bf;
        
        rotateLeft(subL);
        rotateRight(parent);

        if(bf == -1){
            subL.bf = subLR.bf = 0;
            parent.bf = -1;
        }else{
            subL.bf = -1;
            subLR.bf = parent.bf = 0;
        }

    }

最后来验证一下是否成功,主要是根据中序遍历的结果是否有序和左右子树的高度差不超过一来进行验证


五 . AVL树性能分析

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


总结

大家多多理解,我们下一篇博客见

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

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

相关文章

process control 化学工程 需要用到MATLAB的Simulink功能

process control 化学工程 需要用到MATLAB的Simulink功能 所有问题需要的matlab simulink 模型 WeChat: ye1-6688 The riser tube brings in contact the recirculating catalyst with the feed oil, which then vaporizes and splits to lighter components as it flows up th…

【ArcGIS Pro微课1000例】0038:基于ArcGIS Pro的人口密度分析与制图

文章目录 一、人口密度二、人口密度分析1. 点密度分析2. 核密度分析三、结果比对一、人口密度 人口密度是指单位土地面积上居住的人口数,通常以每平方千米或每公顷内的常住人口为单位计算。人口密度同资源、经济密切结合,因此,科学准确地分析人口密度的分布情况,对合理制定…

当你准备开始学习 Java 时,确保已完成以下准备工作,安装Java开发环境并验证通过。

当你准备开始学习 Java 时&#xff0c;确保已完成以下准备工作&#xff1a; a. 安装Java开发环境 下载Java Development Kit (JDK)&#xff1a; 访问Oracle官方网站&#xff0c;选择适用于你操作系统的JDK版本&#xff0c;点击下载。 安装JDK&#xff1a; 下载完成后&#xf…

window获取密码工具

工具getpass.exe 运行输出密码到5.txt 工具gethashes.exe 运行之后输入到6.txt,会得到一个$local 再运行gethashes.exe $local 可以看到加密的账户密码&#xff0c;用工具进行解密就可以得到密码 工具pwdump7 还有其他的mimikatz&#xff0c;msf工具都可以获取。

如何把自己银行卡里的钱转账充值到自己支付宝上?

原文来源&#xff1a;https://www.caochai.com/article-4524.html 支付宝余额是支付宝核心功能之一&#xff0c;主要用于网购支付、线下支付、转账等场景。用户可以将银行卡、余额宝等资金转入或转出至支付宝余额&#xff0c;实现快速转账和支付。 如何把自己银行卡里的钱转账…

案例-某验四代滑块反爬逆向研究二

系列文章目录 第一部分 案例-某验四代滑块反爬逆向研究一 第二部分 案例-某验四代滑块反爬逆向研究二 文章目录 系列文章目录前言一、js文件加载先后顺序二、每次刷新都会初始化 device_id, 所以追栈可以知道它从哪执行的三、删除node中的检测点&#xff08;vm忽视&#xff09…

【一文讲清楚 Anaconda 相关环境配置】

文章目录 0 前言1 Package 与环境1.1 module1.2 package1.3 环境 2 Conda、Miniconda、Anaconda和Pip & PyPI2.1 Conda2. 2 Miniconda2.3 Anaconda2.3.1 Anaconda Navigator2.3.2 Anaconda PowerShell Prompt & Anaconda Prompt2.3.3 Jupyter notebook 2.4 Pip & P…

深信服实验学习笔记——nmap常用命令

文章目录 1. 主机存活探测2. 常见端口扫描、服务版本探测、服务器版本识别3. 全端口&#xff08;TCP/UDP&#xff09;扫描4. 最详细的端口扫描5. 三种TCP扫描方式 1. 主机存活探测 nmap -sP <靶机IP>-sP代表 2. 常见端口扫描、服务版本探测、服务器版本识别 推荐加上-v参…

PTA NeuDS-数据库题目集

一.判断题 1.在数据库中产生数据不一致的根本原因是冗余。T 解析&#xff1a;数据冗余是数据库中产生数据不一致的根本原因&#xff0c;因为当同一数据存储在多个位置时&#xff0c;如果其中一个位置的数据被修改&#xff0c;其他位置的数据就不一致了。因此&#xff0c;在数据…

【测试开发工程师】TestNG测试框架零基础入门(上)

哈喽大家好&#xff0c;我是小浪。那么今天是一期基于JavaTestNG测试框架的入门教学的博客&#xff0c;从只会手工测试提升到自动化测试&#xff0c;这将对你的测试技术提升是非常大的&#xff0c;有助于我们以后在找工作、面试的时候具备更大的竞争力~ 文章目录 一、什么是T…

【数据结构实验】排序(二)希尔排序算法的详细介绍与性能分析

文章目录 1. 引言2. 希尔排序算法原理2.1 示例说明2.2 时间复杂性分析 3. 实验内容3.1 实验题目&#xff08;一&#xff09;输入要求&#xff08;二&#xff09;输出要求 3.2 算法实现3.3 代码解析3.4 实验结果 4. 实验结论 1. 引言 排序算法在计算机科学中扮演着至关重要的角色…

坚鹏:中国银联公司银行业前沿技术介绍及其数据分析方法实战培训

中国银联公司银行业前沿技术介绍及其数据分析方法实战培训圆满结束 ——借力数字化技术实现基于场景的精准化、场景化、智能化营销 中国银联公司&#xff08;China UnionPay&#xff09;成立于2002年3月&#xff0c;是经国务院同意&#xff0c;中国人民银行批准&#xff0c;在合…

一种太阳能风能市电互补路灯方案介绍

太阳能市电互补路灯是一种环保、节能的照明设施&#xff0c;它利用太阳能进行发电并实现照明。这种路灯在白天吸收阳光并将其转化为电能&#xff0c;到了晚上则利用储存的电能为LED灯提供电力&#xff0c;实现照明功能。下面叁仟智慧将详细介绍太阳能市电互补路灯灯的工作原理和…

人工智能|机器学习——循环神经网络的简洁实现

循环神经网络的简洁实现 如何使用深度学习框架的高级API提供的函数更有效地实现相同的语言模型。 我们仍然从读取时光机器数据集开始。 import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2lbatch_size, num_steps 32, 35 t…

4-20mA高精度采集方案

下载链接&#xff01;https://mp.weixin.qq.com/s?__bizMzU2OTc4ODA4OA&mid2247557466&idx1&snb5a323285c2629a41d2a896764db27eb&chksmfcfaf28dcb8d7b9bb6211030d9bda53db63ab51f765b4165d9fa630e54301f0406efdabff0fb&token976581939&langzh_CN#rd …

明道云伙伴成果与展望

摘要&#xff1a;这篇文章介绍了明道云在过去一年的成果以及未来的计划。明道云将把更多资源和精力投入到伙伴身上&#xff0c;提供更全面的支持&#xff0c;包括产品特性、展业支持和 GTM &#xff08;Go-To-Market&#xff09;支持三个方面。在产品特性方面&#xff0c;明道云…

【数据结构实验】排序(一)冒泡排序改进算法 Bubble及其性能分析

文章目录 1. 引言2. 冒泡排序算法原理2.1 传统冒泡排序2.2 改进的冒泡排序 3. 实验内容3.1 实验题目&#xff08;一&#xff09;输入要求&#xff08;二&#xff09;输出要求 3.2 算法实现 4. 实验结果5. 实验结论 1. 引言 排序算法是计算机科学中一个重要而基础的研究领域&…

03:2440--UART

目录 一:UART 1:概念 2:工作模式 3:逻辑电平 4:串口结构图 5:时间的计算 二:寄存器 1:简单的UART传输数据 A:GPHCON--配置引脚 B:GPHUP----使能内部上拉​编辑 C: UCON0---设置频率115200 D: ULCON0----数据格式8n1 E:发送数据 A:UTRSTAT0 B:UTXHO--发送数据输…

makefile 学习(5)完整的makefile模板

参考自&#xff1a; (1&#xff09;深度学习部署笔记(二): g, makefile语法&#xff0c;makefile自己的CUDA编程模板(2&#xff09;https://zhuanlan.zhihu.com/p/396448133(3) 一个挺好的工程模板&#xff0c;(https://github.com/shouxieai/cpp-proj-template) 1. c 编译流…

linux嵌入式时区问题

目录 操作说明实验参考 最近有个针对时区的需求&#xff0c;研究了下。 查询网上的一些设置&#xff0c;发现基本都是系统中自带的一些文件&#xff0c;然后开机时解析&#xff0c;或者是有个修改的命令。 操作 但针对嵌入式常用到的 busybox 制作的最小系统&#xff0c;并没…