JavaDS —— AVL树

news2024/10/2 8:32:38

前言

本文章将介绍 AVL 树的概念,重点介绍AVL 树的插入代码是如何实现的,如果大家对 AVL 树的删除(还是和二叉搜索树一样使用的是替换删除法,然后需要判断是否进行旋转调整)感兴趣的话,可以自行去翻阅其他资料~~

概念

回顾二叉搜索树

之前我们就了解到二叉搜索树中序遍历的时候数据是有序的,这是由于二分搜索树具有以下性质:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

最好的搜索时间复杂度为 O(logN),但是如果插入的数据是有序或者逆序的时候,二叉搜索树就会变成一颗单分支的二叉树,搜索的时间复杂度也最差,为 O(N)

那么能不能让二叉搜索树在插入结点的时候就能始终保持平衡,也就是保持一颗饱满的二叉树形态呢?

这就是我们要学习的 AVL 树

AVL 树

两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年 发明了一种解决上述二叉搜索树存在的问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树具有以下性质:
AVL 树同时具有二叉搜索树的性质
它的左右子树都是AVL树
左右子树高度之差(简称平衡因子)的绝对值不超过 1 (-1、0、1),即始终保持高度平衡
在这里插入图片描述

如果一棵二叉搜索树是高度平衡的,它就是AVL树。

平衡因子

平衡因子就是左右子树高度之差

这里我定义平衡因子等于右子树高度减去左子树的高度

以下图为例:
在这里插入图片描述
A 结点右子树高度等于左子树高度,平衡因子为0
B 结点右子树高度减左子树高度等于 -1,平衡因子为 -1
C 结点右子树高度减左子树高度等于 1,平衡因子为 1
D 结点右子树高度等于左子树高度,平衡因子为0
E 结点右子树高度等于左子树高度,平衡因子为0

结点定义

和普通的树结点一样,具有左引用,右引用,数据val,以及构造方法
但是 AVL 树还要再加一个平衡因子(Balance Factor)简写为 bf
还有一个双亲结点的引用(和平衡因子一样,插入的时候要用到)

public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode parent;
    int bf; //平衡因子 右子树高度减去左子树高度

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

插入实现

首先完成结点的插入工作,这里的插入和之前我们在二叉搜索树已经学习过,这里就直接上代码,如果不了解的可以点开这个连接:JavaDS —— 二叉搜索树、哈希表、Map 与 Set

    public boolean insert(int val) {
        TreeNode node = new TreeNode(val);

        //如果根节点本身为空就直接赋值
        if(root == null) {
            root = node;
            return true;
        }

        TreeNode prev = null;
        TreeNode cur = root;

        //找到新结点的位置
        while(cur != null) {
            if(cur.val == val) {
                //相同的数据无法再次插入
                return false;
            } else if(cur.val < val) {
                prev = cur;
                cur = cur.right;
            } else {
                prev = cur;
                cur = cur.left;
            }
        }

        //插入结点
        if(prev.val > val) {
            prev.left = node;
        } else {
            prev.right = node;
        }
        node.parent = prev;

    }

现在就是调整平衡因子(这里我设定为右子树高度减左子树高度),如果你是插入到左子树的话,那平衡因子就要自减,如果是插入到右子树的话,那平衡因子就要自增。

首先就要先拿到 node 的位置,将cur 重置为 node

cur = node;
			//左子树-- 右子树++
            if(prev.left == cur) {
                prev.bf--;
            } else {
                prev.bf++;
            }

调节完平衡因子之后,此时调节过的平衡因子有五种情况:0 、1 、-1 、2 、-2

如下图所示:红色的结点为新插入的结点,灰色的结点则是已经存在的
在这里插入图片描述
在这里插入图片描述


如果调节过后,平衡因子依旧为 0 ,则说明该树本身就已经平衡了,不需要进行旋转调整.

如果调节过后的平衡因子为 1 或者 -1,说明该结点的兄弟结点为空,这个结点的插入可能会导致不平衡,需要我们继续向上调整平衡因子,这时候我们会使用 parent 引用,让prev 和 cur 一起向上移动,大家就会想到使用循环来调整平衡因子。

循环的部分代码如下:

        cur = node;

        //调整平衡因子与AVL树
        while(prev != null) {
            //左子树-- 右子树++
            if(prev.left == cur) {
                prev.bf--;
            } else {
                prev.bf++;
            }

            //检查是否需要旋转
            if(prev.bf == 0) {
                //平衡因子为0,说明树已经平衡,不用调整
                break;
            } else if(prev.bf == -1 || prev.bf == 1) {
                //如果出现 -1 或者 1 则说明树的平衡性已经被新结点影响到,需要继续调整
                cur = prev;
                prev = prev.parent;
            }
        }

如果调节过后的平衡因子为 2 或者 -2,说明该树出现不平衡,需要进行旋转调整

大家可以记一下旋转的口诀,旋转内容会在下面继续讲解:

左左型:右单旋
右右型:左单旋
左右型:左右双旋
右左型:右左双选

 		else {
                //此时怕平衡因子有两种情况2 或者 -2,需要旋转重新建立平衡树
                if(prev.bf == 2) {
                    //说明右子树过高
                    if(cur.bf == 1) {
                        //右右型 左单旋
                        rotateLeft(prev);
                    } else if(cur.bf == -1) {
                        //右左型 右左双旋
                        rotateRL(prev);
                    }

                } else {
                    //此时 prev.bf == -2 说明左子树过高
                    if(cur.bf == 1) {
                        //左右型,左右双旋
                        rotateLR(prev);
                    } else if(cur.bf == -1) {
                        //左左型, 右单旋
                        rotateRight(prev);
                    }

                }

                //旋转完成后,树已经平衡,直接退出循环
                break;
            }

经过旋转过后,树就会平衡,就无需继续调整平衡因子,直接跳出循环即可。

旋转实现

下面所有的实例图绿色的数字表示经过旋转后平衡因子变为多少

左单旋

当新插入的结点插在某个结点的右孩子的右子树时(这个简称为右右型),那么这个某个结点采用左单旋:

简单情况:
在这里插入图片描述
我们需要让 prev 成为 cur 的左孩子,这时候我们需要修改 prev 的 parent 引用, cur 的左孩子引用以及parent 引用,并且cur 结点的 parent 引用需要改成原来 prev 的parent引用(记为 pParent),所以我们事先就要保存好pParent,最后我们要将pParent 的左引用或者右引用 来连接cur (这里需要判断一下),最后的最后这两个结点的平衡因子置为 0

特殊情况:如果prev 本身就是 根节点的话,那么根节点的引用要变成 cur 的。

如果 cur 的左孩子本身就存在呢?
在这里插入图片描述
那么我们需要将 cur 左孩子结点先保存起来,让 prev.right = cur 的左孩子结点,并且左孩子结点的 parent 的引用要修改为 prev,所以这里引申出我们需要判断 cur 的左孩子结点存不存在,如果不存在则不需要修改其 parent 引用,避免发生空指针异常

    //左单旋
    private void rotateLeft(TreeNode prev) {
        TreeNode pParent = prev.parent;
        TreeNode cur = prev.right;
        TreeNode curL = cur.left;

        prev.right = curL;
        if(curL != null) {
            curL.parent = prev;
        }
        cur.left = prev;
        prev.parent = cur;
        cur.parent = pParent;

        if(prev == root) {
            root = cur;
        } else if(pParent.left == prev) {
            pParent.left = cur;
        } else {
            pParent.right = cur;
        }

        //调整平衡因子
        cur.bf = prev.bf = 0;
    }

右单旋

当新插入的结点插在某个结点的左孩子的左子树时(这个简称为左左型),那么这个某个结点采用右单旋:

简单情况:
在这里插入图片描述
这个和左单旋很相似,大家可以类比左单旋。

特殊情况:如果 prev 本身就是根节点的话就要修改根结点的引用。

还有,如果 cur 自身就有右孩子:让 prev.left = cur 的右孩子结点,并且判断右孩子结点是否为空,不为空的话将 parent 的引用要修改为 prev,
在这里插入图片描述

    //右单旋
    private void rotateRight(TreeNode prev) {
        TreeNode pParent = prev.parent;
        TreeNode cur = prev.left;
        TreeNode curR = cur.right;

        prev.left = curR;
        if(curR != null) {
            curR.parent = prev;
        }
        cur.right = prev;
        prev.parent = cur;
        cur.parent = pParent;
        if(prev == root) {
            root = cur;
        } else if(pParent.left == prev) {
            pParent.left = cur;
        } else {
            pParent.right = cur;
        }

        //调整平衡因子
        cur.bf = prev.bf = 0;
    }

左右双旋

当新插入的结点插在某个结点的左孩子的右子树时(这个简称为左右型),那么这个某个结点采用左右双旋:

简单情况:
在这里插入图片描述

左右双旋是指先进行左单旋,再进行右单旋,当然你也可以一步到位,我这里采用分步
所以左右双旋需要先对 cur 调用左单旋,再对 prev 调用右单旋

特殊情况:由于在单旋代码中我们已经处理好根节点和prev 的parent 结点了,但是这三个结点的平衡因子最后一定是 0 吗???
这次我们讨论的是如果是下面两种复杂的情况时,我们就需要修改一下某些结点的平衡因子:
在这里插入图片描述
在这里插入图片描述
进行完两个单旋转后,最后这三个结点的平衡因子都为0,但是看到上面的情况之后,其实并不是都为0,所以我们在进行旋转的时候就要先保存好cur 的右孩子的平衡因子,等到旋转结束后,就要调整平衡因子。

当 cur.right.bf = -1 时,prev.bf = 1;
当 cur.right.bf = 1 时,cur.bf = -1

    //左右双旋
    private void rotateLR(TreeNode prev) {
        TreeNode cur = prev.left;
        TreeNode curR = cur.right;
        int bf = curR.bf;

        rotateLeft(cur);
        rotateRight(prev);

        //调整平衡因子
        if(bf == 1) {
            cur.bf = -1;
        } else if(bf == -1) {
            prev.bf = 1;
        }

        //bf 为 0 的时候,不需要调整
    }

右左双旋

当新插入的结点插在某个结点的右孩子的左子树时(这个简称为右左型),那么这个某个结点采用右左双旋:

和左右双旋是类似的,这里就给出三种情况的图片给大家参考:
简单情况:
在这里插入图片描述
特殊情况:
当 cur.left.bf = -1 时,cur.bf = 1;
在这里插入图片描述


当 cur.left.bf = 1 时,prev.bf = -1;

在这里插入图片描述

    //右左双旋
    private void rotateRL(TreeNode prev) {
        TreeNode cur = prev.right;
        TreeNode curL = cur.left;
        int bf = curL.bf;

        rotateRight(cur);
        rotateLeft(prev);

        //调整平衡因子
        if(bf == 1) {
            prev.bf = -1;
        } else if(bf == -1) {
            cur.bf = 1;
        }

        //bf 为 0 的时候,不需要调整
    }

测试

写好AVL 树,记得测试自己的代码有没有错误,这里要测试两个东西,第一个是中序遍历的时候是否有序(检测是否为二叉搜索树),第二个东西就是平衡因子有没有错误以及是否是一个高度平衡的二叉树(因为都与高度有关,所以可以放在同一个方法里去写测试代码)。

最终代码

public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode parent;
    int bf; //平衡因子 右子树高度减去左子树高度

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

public class AVLTree {
    public TreeNode root;

    public boolean insert(int val) {
        TreeNode node = new TreeNode(val);

        //如果根节点本身为空就直接赋值
        if(root == null) {
            root = node;
            return true;
        }

        TreeNode prev = null;
        TreeNode cur = root;

        //找到新结点的位置
        while(cur != null) {
            if(cur.val == val) {
                //相同的数据无法再次插入
                return false;
            } else if(cur.val < val) {
                prev = cur;
                cur = cur.right;
            } else {
                prev = cur;
                cur = cur.left;
            }
        }

        //插入结点
        if(prev.val > val) {
            prev.left = node;
        } else {
            prev.right = node;
        }
        node.parent = prev;

        cur = node;

        //调整平衡因子与AVL树
        while(prev != null) {
            //左子树-- 右子树++
            if(prev.left == cur) {
                prev.bf--;
            } else {
                prev.bf++;
            }

            //检查是否需要旋转
            if(prev.bf == 0) {
                //平衡因子为0,说明树已经平衡,不用调整
                break;
            } else if(prev.bf == -1 || prev.bf == 1) {
                //如果出现 -1 或者 1 则说明树的平衡性已经被新结点影响到,需要继续调整
                cur = prev;
                prev = prev.parent;
            } else {
                //此时怕平衡因子有两种情况2 或者 -2,需要旋转重新建立平衡树
                if(prev.bf == 2) {
                    //说明右子树过高
                    if(cur.bf == 1) {
                        //右右型 左单旋
                        rotateLeft(prev);
                    } else if(cur.bf == -1) {
                        //右左型 右左双旋
                        rotateRL(prev);
                    }

                } else {
                    //此时 prev.bf == -2 说明左子树过高
                    if(cur.bf == 1) {
                        //左右型,左右双旋
                        rotateLR(prev);
                    } else if(cur.bf == -1) {
                        //左左型, 右单旋
                        rotateRight(prev);
                    }

                }

                //旋转完成后,树已经平衡,直接退出循环
                break;
            }
        }

        return true;
    }

    //左右双旋
    private void rotateLR(TreeNode prev) {
        TreeNode cur = prev.left;
        TreeNode curR = cur.right;
        int bf = curR.bf;

        rotateLeft(cur);
        rotateRight(prev);

        //调整平衡因子
        if(bf == 1) {
            cur.bf = -1;
        } else if(bf == -1) {
            prev.bf = 1;
        }

        //bf 为 0 的时候,不需要调整
    }

    //右左双旋
    private void rotateRL(TreeNode prev) {
        TreeNode cur = prev.right;
        TreeNode curL = cur.left;
        int bf = curL.bf;

        rotateRight(cur);
        rotateLeft(prev);

        //调整平衡因子
        if(bf == 1) {
            prev.bf = -1;
        } else if(bf == -1) {
            cur.bf = 1;
        }

        //bf 为 0 的时候,不需要调整
    }

    //右单旋
    private void rotateRight(TreeNode prev) {
        TreeNode pParent = prev.parent;
        TreeNode cur = prev.left;
        TreeNode curR = cur.right;

        prev.left = curR;
        if(curR != null) {
            curR.parent = prev;
        }
        cur.right = prev;
        prev.parent = cur;
        cur.parent = pParent;
        if(prev == root) {
            root = cur;
        } else if(pParent.left == prev) {
            pParent.left = cur;
        } else {
            pParent.right = cur;
        }

        //调整平衡因子
        cur.bf = prev.bf = 0;
    }

    //左单旋
    private void rotateLeft(TreeNode prev) {
        TreeNode pParent = prev.parent;
        TreeNode cur = prev.right;
        TreeNode curL = cur.left;

        prev.right = curL;
        if(curL != null) {
            curL.parent = prev;
        }
        cur.left = prev;
        prev.parent = cur;
        cur.parent = pParent;

        if(prev == root) {
            root = cur;
        } else if(pParent.left == prev) {
            pParent.left = cur;
        } else {
            pParent.right = cur;
        }

        //调整平衡因子
        cur.bf = prev.bf = 0;
    }
}

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

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

相关文章

WaitGroup

第一节&#xff1a;WaitGroup 概述 1. WaitGroup 简介 WaitGroup 是 Go 语言标准库 sync 包中的一个并发同步工具&#xff0c;它用于协调主 goroutine 与多个工作 goroutine 的执行。通过计数器跟踪还未完成的工作 goroutine 的数量&#xff0c;WaitGroup 能够确保主 goroutin…

Mybatis-Plus-常用的注解:@TableName、@TableId、@TableField、@TableLogic

1、TableName 经过之前的测试&#xff0c;在使用MyBatis-Plus实现基本的CRUD时&#xff0c;我们并没有指定要操作的表&#xff0c;只是在Mapper接口继承BaseMapper时&#xff0c;设置了泛型User&#xff0c;而操作的表为user表由此得出结论&#xff0c;MyBatis-Plus在确定操作…

宝塔8.0开心版安装命令

使用方法 Centos安装脚本 yum install -y wget \&\& wget -O install.sh https://BTKXB.com/install/install_6.0.sh \&\& sh install.sh Ubuntu/Debian安装脚本 wget -O install.sh https://BTKXB.com/install/install_6.0.sh \&\& bash install.…

十天口语笔记

看 到 Part 2 的话题是要求描述过去的经历&#xff0c;可以在1 分钟思考时间刚开始时就把-ed写在考官给你记notes的纸上提示自己 01

【MySQL】索引——索引的引入、认识磁盘、磁盘的组成、扇区、磁盘访问、磁盘和MySQL交互、索引的概念

文章目录 MySQL1. 索引的引入2. 认识磁盘2.1 磁盘的组成2.2 扇区2.3 磁盘访问 3. 磁盘和MySQL交互4. 索引的概念4.1 索引测试4.2 Page4.3 单页和多页情况 MySQL 1. 索引的引入 海量表在进行普通查询的时候&#xff0c;效率会非常的慢&#xff0c;但是索引可以解决这个问题。 -…

COMSOL金属氢化物-放氢过程

在此记录下放氢过程的软件设置思路 1、采用的是"达西定律""层流" 物理场&#xff0c;其中"层流"物理场选择了”弱可压缩流动“&#xff0c;这里主要是选择”可压缩流动“的话&#xff0c;算出来的瞬时流量值跟实测差距太大了。 2、设置"达西…

【Elegant Programming (优雅的编程)】如何用合理的封装优雅的化解三层以上的 if-else ?

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

数据结构与算法 - 二叉树

1. 概述 二叉树是这么一种树状结构&#xff1a;每个节点最多有两个孩子&#xff0c;左孩子和右孩子 完全二叉树&#xff1a;是一种二叉树结构&#xff0c;除了最后一层以外&#xff0c;每一层都必须填满&#xff0c;填充时要遵循从左到右 平衡二叉树&#xff1a;是一种二叉树…

基础算法之模拟

1P1093 [NOIP2007 普及组] 奖学金 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P1093https://www.luogu.com.cn/problem/P1093 #include<iostream> #include<algorithm> using namespace std; struct stu {int num;//编号int c…

尚品汇-首页三级分类实现-nginx静态代理生成的静态页面(二十六)

目录&#xff1a; &#xff08;1&#xff09;问题详解 &#xff08;2&#xff09;首页商品分类实现 &#xff08;3&#xff09;修改web-all模块 &#xff08;4&#xff09;页面渲染 &#xff08;1&#xff09;问题详解 &#xff08;2&#xff09;首页商品分类实现 前面做了…

【书生大模型实战营(暑假场)】入门任务三 Python 关卡

入门任务二 Python 关卡 参考&#xff1a; 教程任务 1 闯关任务 1.1 使用 Python 实现 wordcount import stringdef wordcount(text):# 去除标点符号text text.translate(str.maketrans(, , string.punctuation))# 转换为小写text text.lower()# 分割字符串成单词列表wo…

CH571F蓝牙orUSB摇杆鼠标

演示视频&#xff1a; 短视频刷个爽 程序基本上是基于官方的例程上改的&#xff0c;用到的例程有&#xff1a;蓝牙的HID_Mouse,USB的CompoundDev&#xff0c;还有ADC&#xff0c;按键中断。 主要原理 就是ADC采集采集摇杆电压&#xff0c;通过蓝牙HID或者USB的HID发送给电脑或…

文心一言 VS 讯飞星火 VS chatgpt (317)-- 算法导论22.3 9题

九、请给出如下猜想的一个反例&#xff1a;如果有向图G包含一条从结点u到结点v的路径&#xff0c;则任何对图G的深度优先搜索都将导致v.d⩽u.f。如果要写代码&#xff0c;请用go语言。 文心一言&#xff1a; 首先&#xff0c;我们需要澄清问题中的几个关键点。在图的深度优先…

想做抖音短视频,视频素材去哪里找啊?

各位抖音上的短视频创作者们&#xff0c;是否曾幻想过自己的作品能够在全网爆火&#xff0c;却常因为缺少那些能够让视频更加生动的素材而感到困扰&#xff1f;不用担心&#xff0c;今天我要为大家介绍几个优秀的视频素材网站&#xff0c;让你的抖音之路顺风顺水&#xff01; …

Linux系统中的高级用户空间与内核空间交互技术

Linux作为一种开源操作系统&#xff0c;具有良好的稳定性、安全性和自定制性&#xff0c;因而在各种设备和场景中得到广泛应用。作为Linux系统的核心组成部分&#xff0c;内核空间与用户空间交互技术对系统性能和功能扩展起着关键作用。本文将深入探讨Linux系统中的高级用户空间…

Vue Vine:带给你全新的 Vue 书写体验!

你好&#xff0c;我是 Kagol&#xff0c;个人公众号&#xff1a;前端开源星球。 上个月和 TinyVue 的小伙伴们一起参加了 VueConf 24 大会&#xff0c;有幸认识沈青川大佬&#xff0c;并了解了他的 Vue Vine 项目&#xff0c;Vue Vine 让你可以在一个文件中通过函数方式定义多…

系统化学习 H264视频编码(05)码流数据及相关概念解读

说明&#xff1a;我们参考黄金圈学习法&#xff08;什么是黄金圈法则?->模型 黄金圈法则&#xff0c;本文使用&#xff1a;why-what&#xff09;来学习音H264视频编码。本系列文章侧重于理解视频编码的知识体系和实践方法&#xff0c;理论方面会更多地讲清楚 音视频中概念的…

Nginx进阶-常见配置(二)

一、nginx 日志配置 nginx 日志介绍 nginx 有一个非常灵活的日志记录模式,每个级别的配置可以有各自独立的访问日志, 所需日志模块 ngx_http_log_module 的支持&#xff0c;日志格式通过 log_format 命令来定义&#xff0c;日志对于统计和排错是非常有利的&#xff0c;下面总…

【TwinCAT3教程】TwinCAT3 PLC 简单程序编写与调试

一、PLC 简单程序编写 1.1 新建TwinCAT3项目 (1)打开 TwinCAT 3,点击 New TwinCAT Project 新建 TC3 项目。 (2)选择 TwinCAT Project,输入项目名称和项目保存路径,然后点击确定。 1.2 添加PLC项目 1.2.1 步骤 (1)在树形资源管理器右键点击 PLC,选择 添加新项 新…

STM32F28335实验:继电器

继电器控制电机&#xff1a; 5s启动 5s停止 循环 管脚图&#xff1a; 管脚用的是GPIO15 驱动&#xff1a; beep.c /** leds.c** Created on: 2024年8月2日* Author: Administrator*/#include<relay.h>/***************************************************…