首页 > 编程语言 >高阶数据结构(Java):AVL树插入机制的探索

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

时间:2024-08-14 19:27:43浏览次数:16  
标签:bf Java parent AVL subR 平衡 数据结构 节点

目录

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

标签:bf,Java,parent,AVL,subR,平衡,数据结构,节点
From: https://blog.csdn.net/2401_83595513/article/details/141173313

相关文章

  • Java 大文件IO操作效率对比【我说说 你瞅瞅】
    Java文件IO操作效率对比注:本文只做时间消耗层面对比,内存占用层面需要特别关注!参数说明文件总大小:2,111,993,850字节(2.11 GB)staticStringdefaultFilePath="/tmp/data-24081412.json";缓冲区大小:8192字节staticintdefaultByteLength=1024*8;示例介绍通过......
  • JAVA字段审计功能-对比修改前后变化并使用枚举Enums进行翻译
    最近接到了一个业务是,审计客户和合同的字段变化,要明细到使用系统的人员能看懂(大概就是我们存入数据库是12什么的进行翻译)返回的信息大概就是:客户A的客户状态从客户状态A 修改成了 客户状态B,客户性别从客户性别A变成了客户性别B。我实现的思路大概就是:1、获取到......
  • java反射简介
    1.反射定义 反射是一种可以间接操作目标对象的机制。当使用反射时,JVM 在运行的时候才动态加载类,对于任意类,知道其属性和方法,并不需要提前在编译期知道运行的对象是谁,允许运行时的Java程序获取类的信息并对其进行操作。2.获取类的四个方式 3.class从类中获取构造器......
  • 计算机毕业设计推荐-基于JAVA的航空机票预定管理系统
    ......
  • 进阶 Java冒泡排序递归法 有点难度哦
    简介这里有用到递归的冒泡排序思路,难度对新手很大,不明白递归和冒泡排序的小伙子可以先看看我的其他两个文章。连接在这里:Java冒泡排序https://blog.csdn.net/ouhexie/article/details/140985984?spm=1001.2014.3001.5501Java递归算法https://blog.csdn.net/ouhexie/articl......
  • 基于Java的校园外卖系统设计与实现。开题报告+答辩PPT+万字论文
    准备毕业设计的时候到了,相信大部分宝子们还没有头绪吧。看完本文会让你受益匪浅。一、项目介绍 本系统是面向所有人的外卖点餐系统。系统内的角色分为管理员和前台用户。管理员有权登录管理端进行如员工信息管理、分类、菜品与套餐管理、查看订单详情及编辑个人资料等操作......
  • java+testng+selenium实现测试用例过程的录制,生成GIF。
    1.功能需求:支持灵活配置:因为本身已有用例执行失败的截图功能,所以需要支持针对单条测试用例的配置;支持testng框架xml多线程的执行;录制内容文件小、支持调整录制每帧间隔、每条用例录制最大时长(避免用例元素未定位到时长时间录制)。2.灵活配置实现创建注解,通过在测试用......
  • JavaSE基础知识分享(六)
    写在前面前面讲的是面向对象中的多态这部分,下面让我们来看看java中常用类这部分的内容!常用类Object概述:是Java中所有类的父类,包括自己定义的类和数组都继承自Object类。成员方法hashCode()获取对象地址值的int类型形式。getClass()获取对象的类的字节码文件对......
  • java流程控制之选择结构
    if单选择结构:我们很多时候需要去判断一个东西是否可行,然后我们才去执行,这样一个过程在程序中用if语句来表达。语法为:if(布尔表达式){//为true执行语句}if双选择结构:两个判断,if-else。语法为:if(布尔表达式){//为true执行语句}else{//为......
  • java后端详解中
    1.日期时间yyyy表示当天所在的年YYYY代表是weekinwhichyear,表示当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的YYYY就是下一年。表示日期和时间的格式:newSimpleDateFormat("yyyy-MM-dd:HH:mm:ss")1)表示月份是大写的M;2)表示分钟则是小写的m;3)24小......