高阶数据结构(Java):AVL树插入机制的探索

news2024/11/13 9:35:58

目录

1、概念

1.1 什么是AVL树

2.1 平衡因子

3、AVL树节点的定义

4、AVL树的插入机制

4.1 初步插入节点

4.2 更新平衡因子

4.3 提升右树高度

4.3.1 右单旋

4.3.2 左右双旋

4.4 提升左树高度

4.4.1 左单旋

 4.4.2 右左双旋

5、AVL树的验证

6、AVL树的删除


1、概念

1.1 什么是AVL树

AVL树是一颗二叉搜索树,且树中每个结点的左右子树高度之差的绝对值不超过 1。

二叉搜索树中,在数据乱序情况下其展现为一颗完全二叉树,具备优秀的数据查找能力,时间复杂度可以达到树的高度O(logN);但是在有序或接近有序的情况下,二叉搜索树为退化为一颗单分支树,其数据查找的时间复杂度也将退化为O(N),相当于顺序表,效率低下。

为解决二叉搜索树在极端情况下查找效率低下的问题,AVL树应运而生。

AVL树可以说是二叉搜索树的改良,在每次插入节点时保证每个节点的左右子树高度之差的绝对值不超过1,一旦超过1就要进行旋转调整操作,这样就能够降低树的高度,因此,提高了查找效率。

在AVL树中,其树高始终为O(logN),所以不管是在任何情况下,其查找效率始终稳定为O(logN)。

本篇博客就带大家一起探索AVL树其优雅搜索性能下的数据插入机制并完成代码实现。

2.1 平衡因子

平衡因子即为左右子树高度之差。

AVL树中每个节点的平衡因子都要满足绝对值 <= 1,在插入数据时一旦出现平衡因子 > 1 的情况就要立即对树做出旋转操作,使其重新调整为AVL树。

3、AVL树节点的定义

要实现AVL树,那么节点的定义是必不可少的,定义为主类AVLTree的静态内部类。

在AVL树中,使用孩子双亲表示法,并在每个节点中维护一个平衡因子。

static class TreeNode {
        public int val;//节点值
        public int bf;//平衡因子
        public TreeNode left;//左孩子
        public TreeNode right;//右孩子
        public TreeNode parent;//父节点

        //构造方法
        public TreeNode(int val) {
            this.val = val;
        }
    }

4、AVL树的插入机制

AVL树也是一颗二叉搜索树,所以新节点的插入操作与二叉搜索树一致。

AVL树的难点在于:插入新节点后如何判断是否仍满足AVL树、不满足AVL树后的旋转调整操作、调整完成后平衡因子的更新。

这些都是AVL树插入机制中的难点,接下来我们一步一步攻克。

4.1 初步插入节点

AVL树也是一颗二叉搜索树,所以新节点的插入操作与二叉搜索树一致,这里不再赘述,在下文给出代码。

4.2 更新平衡因子

新节点插入后,判断是否仍为AVL树,那就需要判断平衡因子bf是否 <= 1,也就是说需要对平衡因子进行更新再判断。

在本篇代码实现中,平衡因子定义为:右子树高度 - 左子树高度。

在插入节点后,我们需要更新平衡因子,在更新后,检查是否满足平衡条件,不满足则做出调整,具体过程如下:

  1. cur指针从新插入的节点node开始,向上更新平衡因子(整体为一个循环)
  2. 判断cur是其父节点parent的左孩子还是右孩子:若cur为其的左孩子则parent.bf-- ;若为其右孩子则parent.bf++。
  3. 若parent.bf = 0,说明新插入的节点使当前parent子树平衡,也不会影响上层树的平衡因子,而上层树本来就是平衡的,故当前整棵树仍为AVL树,插入成功,break即可。
  4. 若parent.bf = 1,只能说明当前parent子树是平衡的,而新插入的结点可能会导致到上层树不平衡,所以需要继续向上检查(cur = parent,parent = parent.parent),进行平衡因子的更新。
  5. 当平衡因子出现 >1 的情况时(parent.bf > 1),我们就需要对树进行旋转操作使其重新调整为AVL树(下文细讲,重点操作!!!),调整完成后整棵树旧平衡,break即可。
  6. parent.parent == null时,说明整棵树仍平衡,结束循环。
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.left;
            }else if (cur.val < val) {
                parent = cur;
                cur = cur.right;
            }else {
                //节点不能重复
                return false;
            }
        }
        //cur == null
        if (parent.val > val) {
            parent.left = node;
        }else {
            parent.right = node;
        }
        node.parent = parent;

        cur = node;
        //更新平衡因子
        while (parent != null) {
            if (parent.left == cur) {
                parent.bf--;
            }else {
                parent.bf++;
            }
            if (parent.bf == 0) {
                break;
            }
            if (parent.bf == 2 || parent.bf == -2) {
                if (parent.bf == -2) {
                    //左树高 --》 降低左树高度
                    if (cur.bf == -1) {
                        //右旋
                        rotateR(parent);
                    }else {
                        //左右双旋 cur.bf == 1
                        rotateLR(parent);
                    }
                }else {
                    //右树高 --》 降低右树高度
                    if (cur.bf == 1) {
                        //左旋
                        rotateL(parent);
                    }else {
                        //右左双旋 cur.bf = -1
                        rotateRL(parent);
                    }
                }
                //调整完成
                break;
            }
            cur = parent;
            parent = parent.parent;
        }
        return true;
    }

4.3 提升右树高度

插入节点后更新平衡因子时若发现了parent.bf == -2的节点,则说明此时的树已不满足AVL树,且右子树高度-左子树高度=-2,说明parent节点的左子树高,右子树低,需要进行相关旋转操作,使树重新归于平衡状态。

4.3.1 右单旋

右单旋:即将部分节点旋转调整到右子树中,进而提升右子树高度,降低左子树高度。

当parent.bf == -2时,且cur.bf == -1时,这种情况下我们可以通过一次右单旋操作就可以使树重新归于平衡状态。

我们将右单旋操作定义为一个单独的方法抽象出来,形参为平衡因子 >1 的节点(即parent节点),subL为parent的左孩子,subLR为subL的右孩子。

  1. 我们定义parent的左孩子为subL,subL的右孩子为sunLR,通过画图我们发现,在旋转完成后parent节点成为了subL的右孩子,sunLR成为了parent的左孩子,我们可以改变连接关系完成旋转操作。
  2. 在旋转完成之后,需要更新平衡因子,细心观察可以发现,仅仅只有两个节点的平衡因子发生了改变,parent和subL节点,且均改变为0,进一步说明旋转完成后树重新归于平衡。
  3. 旋转过程中需要注意一些细节,比如:sunLR可能不存在,sunLR存在时(不为null)才可修改其parent域为parent;旋转前parent可能为根节点;旋转前parent可能具有父节点,这样也需要修改subL的parent域。
    /**
     * 右单旋
     * @param parent:bf == 2的节点
     */
    private void rotateR(TreeNode parent) {
        TreeNode subL = parent.left;
        TreeNode subLR = subL.right;

        //进行连接关系的修改,完成旋转操作
        parent.left = subLR;
        subL.right = parent;
        if (subLR != null) {
            //只有subLR存在时才可访问
            subLR.parent = parent;
        }
        TreeNode pParent = parent.parent;
        parent.parent = subL;
        //旋转前parent可能具有父节点,需要修改subR的父亲指向
        subL.parent = pParent;
        if (pParent != null) {
            if (pParent.left == parent) {
                pParent.left = subL;
            }else {
                pParent.right = subL;
            }
        }else {
            //parent为根节点时
            root = subL;
        }
        //修改平衡因子
        subL.bf = 0;
        parent.bf = 0;
    }

4.3.2 左右双旋

当parent.bf == -2时,且cur.bf == 1时,此时左树高度高于右树,但这时仅仅通过一次右单旋操作是不能使树重新平衡的。

此时需要进行左右双旋操作:先对cur树左单旋,再对parent树右单旋。

我们将左右双旋操作抽象成一个方法单独拿出来,形参为平衡因子 >1 的节点(即parent节点),定义subL为parent的左孩子,subLR为subL的右孩子。

此时,我们已经写出了单旋的代码,在这里直接调用即可,但是麻烦的问题又来了,经过两次的单旋操作后,我们发现节点平衡因子已经与实际不匹配了,这里该怎办呢?

其实,旋转后平衡因子的值其实与subLR旋转前的平衡因子数值有关,这里需要分情况讨论:

  1. 若旋转前subLR.bf == 1,则双旋后subLR.bf = 0,parent.bf = 0,subL.bf = -1。
  2. 若旋转前subLR.bf == -1,则双旋后subLR.bf = 0,parent.bf = 1,subL.bf = 0。
  3. 若旋转前subLR.bf == 0,则双旋后subLR.bf = 0,parent.bf = 0,subL.bf = 0。

情况一:若旋转前subLR.bf == 1,则双旋后subLR.bf = 0,parent.bf = 0,subL.bf = -1。

情况二:若旋转前subLR.bf == -1,则双旋后subLR.bf = 0,parent.bf = 1,subL.bf = 0。

情况三:若旋转前subLR.bf == 0,则双旋后subLR.bf = 0,parent.bf = 0,subL.bf = 0。

/**
     * 左右双旋
     * @param parent:bf == 2的节点
     */
    private void rotateLR(TreeNode parent) {
        TreeNode subL = parent.left;
        TreeNode subLR = subL.right;
        int bf = subLR.bf;
        
        rotateL(subL);
        rotateR(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;
        }
        //注意这里没有修改bf == 0情况时的平衡因子,
        //是因为在上面进行双旋时平衡因子已经被修改好了
    }

4.4 提升左树高度

若插入节点的父节点的平衡因子为0,说明整棵树仍旧平衡,插入成功直接break即可。

插入节点后更新平衡因子时若发现了parent.bf == 2的节点,则说明此时的树已不满足AVL树,且右子树高度-左子树高度=2,说明parent节点的右子树高,左子树低,需要进行相关旋转操作,使树重新归于平衡状态。

4.4.1 左单旋

左单旋:即将部分节点旋转调整到左子树中,进而提升左子树高度,降低右子树高度。

当parent.bf == 2时,且cur.bf == 1时,这种情况下我们可以通过一次左单旋操作就可以使树重新归于平衡状态。

同样将左单旋操作定义为一个单独的方法抽象出来,形参为平衡因子 >1 的节点(即parent节点),subR为parent的右孩子,subRL为subR的左孩子。

  1. 我们定义parent的右孩子为subR,subR的左孩子为sunRL,通过画图我们发现,在旋转完成后parent节点成为了subR的左孩子,sunRL成为了parent的右孩子,我们可以改变连接关系完成旋转操作。
  2. 在旋转完成之后,需要更新平衡因子,细心观察可以发现,仅仅只有两个节点的平衡因子发生了改变,parent和subR节点,且均改变为0,进一步说明旋转完成后树重新归于平衡。
  3. 旋转过程中需要注意一些细节,比如:sunRL不为null时才可修改其parent域为parent;旋转前parent可能为根节点;旋转前parent可能具有父节点,这样也需要修改subR的parent域。
    /**
     * 左单旋
     * @param parent:bf == 2的节点
     */
    private void rotateL(TreeNode parent) {
        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;

        //进行连接关系的修改,完成旋转操作
        subR.left = parent;
        parent.right = subRL;
        TreeNode pParent = parent.parent;
        parent.parent = subR;
        if (subRL != null) {
            //只有subRL不为空时才可访问
            subRL.parent = parent;
        }
        //旋转前parent可能具有父节点,需要修改subR的父亲指向
        subR.parent = pParent;
        if (pParent != null) {
            if (parent == pParent.left) {
                pParent.left = subR;
            }else {
                pParent.right = subR;
            }
        }else {
            //parent为根节点时
            root = subR;
        }
        //修改平衡因子
        parent.bf = 0;
        subR.bf = 0;
    }

 4.4.2 右左双旋

当parent.bf == 2时,且cur.bf == -1时,此时右树高度高于左树,但这时仅仅通过一次左单旋操作同样也是不能使树重新平衡的。

我们需要进行右左双旋操作:先对cur树右单旋,再对parent树左单旋。

我们将右左双旋操作定义为一个单独的方法抽象出来,形参为平衡因子 >1 的节点(即parent节点),subR为parent的右孩子,subRL为subR的左孩子。

旋转完成后,平衡因子的数值与subRL旋转前的平衡因子数值有关,我们需要分类讨论。

  • 若旋转前subRL.bf == 1,则双旋后parent.bf = -1,subRL.bf = 0,subR.bf = 0。
  • 若旋转前subRL.bf == -1,则双旋后parent.bf = 1,subRL.bf = 0,subR.bf = 0。
  • 若旋转前subRL.bf == 0,则双旋后parent.bf = 0,subRL.bf = 0,subR.bf = 0。

 情况一:若旋转前subRL.bf == 1,则双旋后parent.bf = -1,subRL.bf = 0,subR.bf = 0。

情况二:若旋转前subRL.bf == -1,则双旋后parent.bf = 1,subRL.bf = 0,subR.bf = 0。

情况三:若旋转前subRL.bf == 0,则双旋后parent.bf = 0,subRL.bf = 0,subR.bf = 0。

/**
     * 右左双旋
     * @param parent:bf == 2的节点
     */
    private void rotateRL(TreeNode parent) {
        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;
        int bf = subRL.bf;

        rotateR(subR);
        rotateL(parent);

        if (bf == -1) {
            parent.bf = 0;
            subRL.bf = 0;
            subR.bf = 1;
        }else if (bf == 1) {
            parent.bf = -1;
            subRL.bf = 0;
            subR.bf = 0;
        }
        //注意这里没有修改bf == 0情况时的平衡因子,
        //是因为在上面进行双旋时平衡因子已经被修改好了
    }

5、AVL树的验证

在实现插入机制后,如何验证一棵树是否是AVL树呢?

我们可以通过以下几个条件判断我们所构建出的是否为AVL树:

  1. 满足二叉搜索树(中序遍历得到升序序列)
  2. 每个节点子树高度差的绝对值 <= 1 
  3. 每个节点的平衡因子的数值正确
 /**
     * 中序遍历 --》 判断是否为二叉搜索树
     * @param root
     */
    public void inorder(TreeNode root) {
        if (root == null) {
            return;
        }
        inorder(root.left);
        System.out.println(root.val);
        inorder(root.right);
    }

    /**
     * 求树的高度
     * @param root
     * @return
     */
    public int height(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int leftH = height(root.left);
        int rightH = height(root.right);

        return Math.max(leftH,rightH)+1;
    }

    public boolean isBalance(TreeNode root) {
        if (root == null) {
            return true;
        }

        int leftH = height(root.left);
        int rightH = height(root.right);
        
        //平衡因子就是 错 的情况下
        if (root.bf != rightH-leftH) {
            return false;
        }
        
        //根节点和左右子树都要平衡
        return leftH-rightH <= 1 && isBalance(root.left) && isBalance(root.right);
    }

6、AVL树的删除

面试时,关于AVL树插入操作的思想和代码问到的比较多,而删除问的就比较少了,最多也就问个思想。这里留下删除操作的思想:

  1. 找到需要删除的节点
  2. 按照搜索树的删除规则删除节点
  3. 更新平衡因子,如果出现了不平衡,进行旋转(单旋,双旋)。

END

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

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

相关文章

uni-app 使用九宫格(uni-grid)布局组件

1、运行环境 开发工具为 HBuilder X 4.23, 操作系统为 Windows 11。Vue.js 版本为 3. 2、操作步骤 首先&#xff0c;登录 HBuilder X。然后用桌面浏览器&#xff0c;访问官网组件网址。 https://ext.dcloud.net.cn/plugin?nameuni-grid 在组件网址右上角、点击“下载插…

出差学习笔记(1)汽车智能大灯一键标定功能

出差学习笔记&#xff08;1&#xff09;汽车智能大灯一键标定功能 今天看到了某公司制作的汽车智能大灯的一键标定功能&#xff0c;好奇&#xff0c;遂问之。 车前的两个大灯如何标定&#xff0c;我们可以将车辆开到一片墙前&#xff0c;将一些动态/静态图形打到墙上&#xff0…

Sublime Text常用快捷键大全

Sublime Text 是一款功能强大且广受欢迎的文本编辑器&#xff0c;其丰富的快捷键支持使得开发者能够更高效地编写和编辑代码。以下是 Sublime Text 中一些常用的快捷键&#xff0c;帮助你更加高效地使用这款工具&#xff1a; 功能分类快捷键 (Windows)快捷键 (Mac)新建文件Ctr…

Unity Shader变体优化与故障排除技巧

在 Unity 中编写着色器时&#xff0c;我们可以方便地在一个源文件中包含多个特性、通道和分支逻辑。在构建时&#xff0c;着色器源文件会被编译成着色器程序&#xff0c;这些程序包含一个或多个变体。变体是该着色器在满足一组条件后生成的版本&#xff0c;这通常会导致线性执行…

实验五之用Processing绘画

1.案例代码如下&#xff1a; import generativedesign.*; import processing.pdf.*; import java.util.Calendar; Tablet tablet; boolean recordPDF false; float x 0, y 0; float stepSize 5.0; PFont font; String letters "Sie hren nicht die folgenden Gesnge…

STM32寄存器点亮跑马灯

硬件状况 DS0灯接线方式 链路&#xff1a;3.3V --- DS0 --- LED0 --- PB5 | --- CPU 分析: PB5, 为高电平, 那么DS0灯 熄灭 PB5, 为低电平, 那么DS0灯 亮 PB5的配置 配置PB5引脚为输出模式, 输出高电平(灭), 输出低电平(亮)&#xff0c;配置为推挽输出 PB5 - GP…

前端开发攻略---图片裁剪上传的原理

目录 ​编辑 1、预览本地图片 2、图片裁剪交互 3、上传裁剪区域 1、预览本地图片 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initia…

面试必备:高频算法与面试题全面解析

干货分享&#xff0c;感谢您的阅读&#xff01; &#xff08;暂存篇---后续会删除&#xff0c;完整版和持续更新见高频面试题基本总结回顾&#xff08;含笔试高频算法整理&#xff09;&#xff09; 备注&#xff1a;引用请标注出处&#xff0c;同时存在的问题请在相关博客留言…

c++割圆法求圆周率

前言 上期的Python(加了turtle 所以带图片)割圆法点赞数量感人 但洛谷那期已经让我飞了 于是我准备掉点头发 以五升六之躯硬刚初三 这期请教了大量的高年级同学 把这个要用到九年级知识点的割圆法搞出来了 不要怕难 像我这样的xxs也能看懂 先声明 割圆法不一定要用循环 …

Open3D PCA法中特征值和特征向量中的关系及应用(原理详细版)

目录 一、概述 1.1定义 1.2特征值与特征向量的关系 1.3特征值和特征向量大小的关系 1.4在PCA法中的应用及实现步骤 二、特征值大小排序的意义 2.1排序的原因 2.2特征值大小及意义 2.3在PCA中的应用 2.4代码示例 三、特征值计算法向量 3.1评判标准 3.2原理 3…

虚拟人实时主持创意互动方案:赋能峰会论坛会议等活动科技互动感

随着增强现实、虚拟现实等技术的不断发展&#xff0c;“虚拟人实时主持”创意互动模式逐渐代替传统单一真人主持模式&#xff0c;虚拟主持人可以随时随地出现在不同活动现场&#xff0c;也可以同一时间在不同分会场中担任主持工作&#xff0c;在峰会、论坛、会议、晚会、发布会…

与LLMs进行在IDE中直接、无需提示的交互是工具构建者探索的一个有希望的未来方向

这个观点在卡内基梅隆大学与谷歌研究人员合作文章 《Using an LLM to Help With Code Understanding》 中提出。 论文地址&#xff1a;https://dl.acm.org/doi/abs/10.1145/3597503.3639187 摘要 理解代码非常具有挑战性&#xff0c;尤其是在新且复杂的开发环境中。代码注…

教程中对DTC(Data Transfer Object)的叙述有可改进之处

图 不 好&#xff0c;看 不 懂&#xff1b; 找 原 文&#xff0c;弄 清 楚。 翻 译 错&#xff0c;图 也 错&#xff1b; 改 几 字&#xff0c;加 一 图。 新 教 程&#xff0c;新 学 习&#xff1b; 新 体 验&#xff0c;从 未 有。

苹果(ios)私钥证书和profile文件申请教程

苹果&#xff08;ios&#xff09;私钥证书&#xff0c;可以理解为p12后缀的苹果证书&#xff0c;我们在苹果开发者中心可以生成cer格式的证书&#xff0c;然后使用mac电脑或者第三方转换成p12后缀格式的私钥证书。 证书profile文件&#xff0c;又叫描述文件&#xff0c;这个文…

无人机的电压和放电速率,你知道吗?

一、无人机电压 无人机电瓶多采用锂电池&#xff0c;其电压范围在3.7伏至44.4伏之间&#xff0c;具体取决于电池的单体电压和串联的电池节数。 单体电压&#xff1a;锂电池的单体电压通常为3.7V&#xff0c;但在满电状态下可能达到4.2V。 串联电池节数&#xff1a;无人机电瓶…

xxl-job定时任务同步点赞数据 + 内网穿透

1.xxl-job基本介绍 1.官方文档 https://www.xuxueli.com/xxl-job/ 2.gitee https://gitee.com/xuxueli0323/xxl-job 2.本地集成xxl-job 1.下载源码包 https://gitee.com/xuxueli0323/xxl-job/tree/6effc8b98f0fd5b5af3a7b6a8995bdcf30de69fc/ 2.导入到项目中 1.作为模…

【待修改】使用GraphRAG+LangChain+Ollama(LLaMa 3.1)知识图谱与向量数据库集成(Neo4j)

如何使用 LLama 3.1(一个本地运行的模型)来执行GraphRAG操作,总共就50号代码。 首先,什么是GraphRAG?GraphRAG是一种通过考虑实体和文档之间的关系来执行检索增强生成的方式,关键概念是节点和关系。 ▲ 知识图谱与向量数据库集成 知识图谱与向量数据库集成是GraphRAG 架…

121 买卖股票的最佳时机

解题思路&#xff1a; \qquad 这个题如果把每一种买卖的可能都算出来的解法时间复杂度在 O ( n 2 ) O(n^2) O(n2)&#xff0c;提交后会超时&#xff0c;所以需要在此基础上进行优化&#xff0c;能否通过一次遍历找出最大利润。 \qquad 对于当前点i&#xff0c;卖出股票所能得…

子域名下部署Java项目到docker中

场景&#xff1a;子域名需要部署Java项目&#xff0c;用于分公司的项目&#xff0c;可支持自定义功能。 拷贝总公司的后台代码 新增数据库并且修改配置环境的数据库连接 启动项目没问题后进行打包 目前我的是打成 jar包 服务器上创建文件&#xff0c;并且创建 dockerfile 文…

springboot整合activity7(一)

一、Spring Boot 集成 Activiti7&#xff08;工作流&#xff09; 此章节首先完成后端的activiti整合&#xff0c;生成工作流所需数据库表&#xff0c;数据库采用mysql。 二、依赖 <dependencies><!-- 引入Activiti7 --><dependency><groupId>org.ac…