AVL树Java实现

news2025/4/17 16:54:07

文章目录

  • AVL树(平衡二插搜索树)
    • 1.概念
      • 二插搜索树
      • AVL树的基本概念
    • 2.AVL数的实现
      • 定义AVL树
      • AVL树的插入
      • AVL树的旋转
        • 右单旋
        • 左单旋
        • 左右双旋
        • 右左双旋
      • 删除元素
    • 3. 验证AVL树
    • 4.AVL树性能分析


AVL树(平衡二插搜索树)

1.概念

二插搜索树

要想了解AVL树,就得先知道二插搜树的性质:

  • 二插搜索树的左子树的值要小于父亲节点的值
  • 二插搜索树的右子树的值要大于父亲节点的值

在这里插入图片描述

如上图就是一棵二插搜索树

  • 二插搜搜树的最小值在左子树,最大值在右子树
  • 二插搜索树的中序遍历时一个有序序列

二插搜索树的查找效率正常情况下是 l o g 2 n log_{2}n log2n,但是在极端情况下如果这颗树转变成了单分支,也就是变成了链表形式,查找效率就是 O ( n ) O(n) O(n)了,这个时候AVL树的优势就来了。

在这里插入图片描述

AVL树的基本概念

AVL树又叫平衡二插搜索树,二插搜索树的查找效率在极端情况下是比较低的,而AVL树会保证左右子树的高度差的绝对值不会超过1,每次在插入新的节点后都会进行对应的调整,保证树的平衡。

一棵AVL数或者是空树,或者是会具有以下性质:

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

在这里插入图片描述

如果一棵 n n n个节点的二插搜索树的高度是平衡的,那么这个搜索树就是AVL树,那么可以它的高度就可以保持为 l o g 2 n log_{2}n log2n,查找的时间复杂度为 l o g 2 n log_{2}n log2n

2.AVL数的实现

定义AVL树

AVL树的每一个节点的定义方式如下:

  • bf为平衡因子:这里采用右子树高度-左子树高度来计算平衡因子(并不是唯一方式)
static class TreeNode {
    // 值
    int val;
    // 左子树高度-右子树高度
    int bf;// 平衡因子
    TreeNode left; //左孩子
    TreeNode right; // 右孩子
    TreeNode parent;// 父节点引用

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

AVL树的插入

AVL树遵循了搜索树的性质,按照搜索树的插入方式进行 插入就行

  • 第一步按照二插搜索树的方式插入节点
  • 第二步就是调整平衡因子,如果发现某一棵子树树已经不平衡就需要进行旋转

插入有几个逻辑:

  • 如果插入的元素比当前节点元素大,就插入到当前节点的右子树
  • 如果插入的元素比当前节点元素小,就插入到当前节点的左子树
  • 如果插入的元素和当前节点元素相等就插入失败
  • 插入新节点后,要将插入节点的parent指向其父节点

接着就是进行平衡因子的调整

  • 如果插入节点是在parent节点的左边,parent节点的平衡因子就减一
  • 同理如果插入节点在parent节点的右边,parent节点的平衡因子就加一

修改平衡因子后就需要进行判断,有三种情况:

  • 当前节点的平衡因子为0,说明插入之前树的平衡因子为 1 1 1或者- 1 1 1,插入节点后平衡因子变成0,此时满足AVL树的性质,插入成功。
  • 当前节点的平衡因子为1或者-1,说明当前子树是平衡的,但并不代表整个AVL树是平衡的,所以要继续从下往上修改对应路径上的平衡因子
  • 如果当前节点的平衡因子为2或者-2,说明当前树已经不平衡需要进行旋转。

在这里插入图片描述

上图是正常情况下的插入,插入元素后整棵树还是平衡的。但如果是其它情况就要进行旋转了:

/**
     * AVL树插入元素
     * @param val
     */
public boolean insert(int val) {
    TreeNode newNode = new TreeNode(val);
    if (root == null) {
        // 第一次插入
        root = newNode;
        return true;
    }
    TreeNode parent = null;
    TreeNode cur = root;
    while (cur != null) {
        parent = cur;
        if (cur.val > val) {
            cur = parent.left;
        } else if (cur.val == val) {
            System.out.println("插入失败元素已经存在");
            return false;
        } else {
            cur = parent.right;
        }
    }
    // 在对应位置插入新元素
    if (parent.val > val) {
        parent.left = newNode;
    } else {
        parent.right = newNode;
    }
    newNode.parent = parent;
    cur = newNode;
    // 调整平衡因子
    while (parent != null) {
        if (parent.left == cur) {
            parent.bf--;
        } else {
            parent.bf++;
        }

        if (parent.bf == 0) {
            // 说明所有树已经平衡,无需调整
            break;
        } else if (parent.bf == 1 || parent.bf == -1) {
            // 当前子树是平衡的,但不能说明整棵树平衡,继续向上调整
            cur = parent;
            parent = cur.parent;
        } else {
            if (parent.bf == 2) {
                if (cur.bf == 1) {
                    // 进行左旋
                    rotateLeft(parent);
                } else {
                    // cur.bf == -1
                    // 右左双旋
                    rotateRL(parent);
                }
            }else {
                //parent.bf == -2
                if (cur.bf == -1) {
                    // 进行右旋
                    rotateRight(parent);
                } else {
                    // cur.bf == 1
                    // 左右双旋
                    rotateLR(parent);
                }
            }
            // 调整完后树已经平衡
            break;
        }

    }
    return true;
}

AVL树的旋转

右单旋

当新节点插入到较高左子树的左侧,此时就会出现平衡因子为-2,其子节点为-1,就需要进行右单旋。右单旋其实就是降低左树的高度来提升右树的高度

在这里插入图片描述

右单旋步骤:

  • 先记录相关节点,parentL、parentLR、pParent
  • parentLR可能出现不存在的情况,如果存在则将该节点的的parent指向当前调整的parent
  • 将parent的left指向parentLR
  • 将parent的parent指向parentL
  • 再将parentL的right指向parent
  • 接着需要判断调整的节点是否是根节点
  • 如果是根节点只需要将,root指向parentL,再将parentL的parent置为null
  • 入过不是根节点则需要判断parent是pParent的左节点还是右节点,对应修改引用
  • 最后再调整对应的平衡因子

在这里插入图片描述

/**
 * 右单旋
 * @param parent
  */
private void rotateRight(TreeNode parent) {
    // 记录对应节点
    TreeNode parentL = parent.left;
    TreeNode parentLR = parentL.right;
    TreeNode pParent = parent.parent;
    // 如果parentLR存在
    if (parentLR != null) {
        parentLR.parent = parent;
    }
    parent.left = parentLR;
    parent.parent = parentL;
    parentL.right = parent;
    // 要调整的是根节点
    if (parent == root){
        root = parentL;
        parentL.parent = null;
    } else {
        // 如果不是根节点就需要判断,当前子树是parent的左子树还是右子树
        if (pParent.left == parent) {
            pParent.left = parentL;
        } else {
            pParent.right = parentL;
        }
        parentL.parent = pParent;
    }
    // 调整平衡因子
    parentL.bf = 0;
    parent.bf = 0;
}

左单旋

当把新节点插入到AVL树中较高右子树的右侧后,调整平衡因子发现节点的平衡因子为2且它的子树为1,此时就需要进行左单旋了。左单旋其实就是降低右树的高度来提升左树的高度。

在这里插入图片描述

左单选步骤:

  • 记录相关节点parentR、parentRL、pParent
  • 让parent的right指向parentRL
  • parentRL可能有不存在的情况,如果存在则让其的parent指向parent
  • 再让parent的parent指向parentR
  • 接着让parentR的left指向parent
  • 判断旋转的是否是根节点,如果是在pParent是为空的,所以要进行特殊判断
  • 最后更新平衡因子

在这里插入图片描述

/**
* 左单旋
* @param parent
*/
private void rotateLeft(TreeNode parent) {
    // 记录对应节点
    TreeNode parentR = parent.right;
    TreeNode parentRL = parentR.left;
    TreeNode pParent = parent.parent;

    // 修改节点
    parent.right = parentRL;
    // 如果parentRL存在
    if (parentRL != null) {
        parentRL.parent = parent;
    }
    parent.parent = parentR;
    parentR.left = parent;
    // 如果旋转的是根节点
    if (parent == root) {
        root = parentR;
        parentR.parent = null;
    } else {
        // 如果旋转的不是根节点就判断旋转的是pParent的左子树还是右子树
        if (pParent.left == parent) {
            pParent.left = parentR;
        } else {
            pParent.right = parentR;
        }
        parentR.parent = pParent;
    }
    // 更新平衡因子
    parent.bf = 0;
    parentR.bf = 0;

}

左右双旋

有些情况下,单纯对树进行左旋或者右旋还是无法保证树是平衡状态,所以此时就需要双旋。比如在较高左子树的右侧插入一个新元素,就需要进行左右双旋。

插入时需要考虑两种情况,一个是插入到左节点和插入到右节点:根据不同情况下的修改负载因子是不一样的,要进行特判。通过parentLR的平衡因子来判断新元素插入左节点还是右节点

插入到较高左子树的右侧的左节点

在这里插入图片描述

插入到较高左子树的右侧的右节点

在这里插入图片描述

假设我们以元素插入到较高左子树的右侧的右节点为例子:

  • 先对parentL进行左单旋

在这里插入图片描述

  • 再对parent进行右单旋

在这里插入图片描述

最后调整平衡因子有两种情况:

  • 通过记录的parentLR的平衡因子来判断修改,如果 p a r e n t L R . b f = = 1 parentLR.bf==1 parentLR.bf==1,说明新元素插入到了右节点,如果 p a r e n t L R . b f = = − 1 parentLR.bf==-1 parentLR.bf==1说明新元素插入到了左节点
  • 如果是记录的 b f = = − 1 bf==-1 bf==1,说明插入的元素在左子树,则需要修改对应3个节点的平衡因子, p a r e n t . b f = 1 parent.bf=1 parent.bf=1 p a r e n t L . b f = 0 parentL.bf=0 parentL.bf=0 p a r e n t L R . b f = 0 parentLR.bf=0 parentLR.bf=0的平衡因子
  • 如果记录的 b f = = 1 bf == 1 bf==1,说明插入的元素在右子树,则需要修对应3个节点的平衡因子, p a r e n t L . b f = − 1 parentL.bf=-1 parentL.bf=1 p a r e n t L R . b f = 0 parentLR.bf=0 parentLR.bf=0 p a r e n t . b f = 0 parent.bf=0 parent.bf=0
/**
     * 进行左右双旋
     * @param parent
     */
private void rotateLR(TreeNode parent) {
    // 记录相关节点
    TreeNode parentL = parent.left;
    TreeNode parentLR = parentL.right;
    int bf = parentLR.bf;
    // 先左旋parent.left
    rotateLeft(parentL);
    // 再右旋parent
    rotateRight(parent);
    // 修改平衡因子
    // 分两种情况
    if (bf == -1) {
        // 插入到较高左子树右侧的左子树
        parent.bf = 1;
        parentL.bf = 0;
        parentLR.bf = 0;
    } else if (bf == 1) {
        //bf == 1
        // 插入到较高左子树右侧的右子树
        parentL.bf = -1;
        parentLR.bf = 0;
        parent.bf = 0;
    }

}

右左双旋

右左双旋是当元素插入在较高右子树的左侧发生的。插入后要考虑两种情况,一个是元素插入在较高右子树的左侧的左节点,另外一种是元素插入在较高右子树的左侧的右节点。

插入到较高右子树左侧的左节点

在这里插入图片描述

插入到较高右子树左侧的右节点

在这里插入图片描述

以插入到较高右子树左侧的右节点为例子

  • 先对parentR进行右旋

在这里插入图片描述

  • 再对parent进行左旋

在这里插入图片描述

最后调整平衡因子有两种情况:

  • 通过记录parentRL的平衡因子来进行判断修改,如果 p a r e n t R L . b f = = 1 parentRL.bf==1 parentRL.bf==1说明新元素插入到了右节点,如果 p a r e n t R L . b f = = − 1 parentRL.bf==-1 parentRL.bf==1说明新元素插入到了左节点
  • 如果parentRL的平衡因子 b f = = 1 bf==1 bf==1,说明新元素插入到了右子树,则需要修改 p a r e n t . b f = = − 1 parent.bf==-1 parent.bf==1 p a r e n t R . b f = 0 parentR.bf=0 parentR.bf=0 p a r e n t R L . b f = 0 parentRL.bf=0 parentRL.bf=0
  • 如果parentRL的平衡因子 b f = = − 1 bf == -1 bf==1,说明新元素插入到了左子树,则需要修改 p a r e n t R . b f = 1 parentR.bf=1 parentR.bf=1 p a r e n t . b f = 0 parent.bf=0 parent.bf=0 p a r e n t R L = 0 parentRL=0 parentRL=0
/**
     * 进行右左双旋
     * @param parent
     */
private void rotateRL(TreeNode parent) {
    // 记录相关节点
    TreeNode parentR = parent.right;
    TreeNode parentRL = parentR.left;
    int bf = parentRL.bf;

    rotateRight(parentR);
    rotateLeft(parent);
    if (bf == -1) {
        parentR.bf = 1;
        parent.bf = 0;
        parentRL.bf = 0;
    } else if (bf == 1) {
        parent.bf = -1;
        parentR.bf = 0;
        parentRL.bf = 0;
    }
}

删除元素

AVL树删除元素,先要找到该元素再进行删除,但这里需要考虑到多种情况。

  • 要删除的是根节点
    • 删除的节点的左子树为空
    • 删除的节点的右子树为空
    • 删除的节点的左右子树都不为空
  • 要删除的不是根节点
    • 删除的节点的左子树为空
    • 删除的节点的右子树为空
    • 删除的节点的左右子树都不为空

针对左右不为空的情况采用替换删除:

  • 去删除节点的左子树找最大值,或者去删除节点的右子树找最小值
  • 更新平衡因子的时候这里和插入相反的
    • 如果删除后平衡因子是 1 1 1或者 − 1 -1 1,说明调整前的平衡因子是0,修改后变成-1和1,并不影响上一层,依旧是平衡的
    • 如果删除后平衡因子是 0 0 0,说明修改前平衡因子是 b f = = − 1 bf==-1 bf==1或者 b f = = 1 bf==1 bf==1,说明了把高的那一棵子树的节点删掉了,此时当前子树是平衡的,但并不代表上一层就是平衡的,所以要继续向上调整
    • 如果删除后更新平衡因子 b f = = 2 bf == 2 bf==2或者 b f = = − 2 bf == -2 bf==2,说明不平衡需要进行旋转

3. 验证AVL树

验证AVL树采用判断每一个子树的左右子树高度差作为判断(右子树高度 − - 左子树高度),同时验证父节点的平衡因子的是否对应该差值。

才用后序遍历进行减枝,从AVL树的叶子节点从底之顶进行判断,可以避免重复判断,只要有一棵子树不平衡就无需判断其它节点了。

时间复杂度 O ( n ) O(n) O(n)

空间复杂度 O ( n ) O(n) O(n)

/**
     * 判断是否AVL树
     * @return
     */
public boolean isBalanced() {
    return balanced(root) >= 0;
}
public int balanced(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int left = balanced(root.left);
    int right = balanced(root.right);
    if (right-left != root.bf) {
        System.out.println("节点:"+root+" 平衡因子出现问题");
        return -1;
    }
    // 当有一颗子树不平衡时就无需判断其它节点了
    if (left >= 0 && right >= 0 && Math.abs(right-left) < 2) {
        return Math.max(right,left)+1;
    } else {
        return -1;
    }
}

4.AVL树性能分析

AVL是一棵高度绝对平衡的二插搜索树,该树要求每个节点的左右子树高度差的绝对值不能超过1,这样可以保证其查询的时间复杂度为 O ( l o g 2 n ) O(log_{2}n) O(log2n),但如果频繁对AVL进行删除和插入操作,性能是非常低的。插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置 。所以如果需要一种查询速度快且数据有序的数据结构,并且只对这些数据进行查询就可以使用AVL树,一旦设计到插入和删除就不适合使用AVL树了。


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

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

相关文章

HIDS-wazuh 的配置和防御

目录 安装wazuh 常用内容 检测sql注入 主动响应 安装wazuh 本地测试的话建议用ova文件&#xff0c;直接导入虚拟机就能用了 官网&#xff1a;Virtual Machine (OVA) - Installation alternatives 常用内容 目录位置&#xff1a;/etc/ossec 配置文件&…

装箱和拆箱

1. 概念 装箱 将值类型转换成等价的引用类型 装箱的步骤 拆箱 将一个已装箱的引用类型转换为值类型&#xff0c;拆箱操作需要声明拆箱后转换的类型 拆箱的步骤 1&#xff09;获取已装箱的对象的地址 2&#xff09;将值从堆上的对象中复制到堆栈上的值变量中 2. 总结 装箱和拆箱…

表现层消息一致性处理

设计表现层返回结果的模型类&#xff0c; 用于后端与前端进行数据格式统一&#xff0c;也称为前后端数据协议 Data public class R {private Boolean flag;private Object data;private String msg;public R(){}public R(Boolean flag){this.flag flag;}public R(Boolean fla…

如何使用NLP库解析Python中的文本

Python是一种强大的面向对象的编程&#xff08;object-oriented programming&#xff0c;OOP&#xff09;语言&#xff0c;在人工智能领域有着广泛的用途。正是鉴于其实用性&#xff0c;以Google为首的大型科技公司&#xff0c;已经对其开发了Tensorflow等代码库&#xff0c;帮…

相交链表00

题目链接 相交链表 题目描述 注意点 保证 整个链式结构中不存在环函数返回结果后&#xff0c;链表必须 保持其原始结构如果 listA 和 listB 没有交点&#xff0c;intersectVal 为 0 解答思路 两个链表从头开始遍历&#xff0c;如果其是在同一个位置处相交&#xff0c;则在…

(AcWing)没有上司的舞会

Ural 大学有 NN 名职员&#xff0c;编号为 1∼N。 他们的关系就像一棵以校长为根的树&#xff0c;父节点就是子节点的直接上司。 每个职员有一个快乐指数&#xff0c;用整数 Hi 给出&#xff0c;其中 1≤i≤N。 现在要召开一场周年庆宴会&#xff0c;不过&#xff0c;没有职…

智能问答FAQ的原始问答数据怎么整理?

整理智能问答FAQ的原始数据是构建一个智能问答系统的重要步骤之一。 如何整理原始问答数据以及如何将其转化为智能问答系统 1. 收集原始数据 收集原始数据是整理智能问答FAQ的第一步。可以从以下途径收集原始数据&#xff1a; 网络搜索&#xff1a;通过搜索引擎、论坛、社交…

小白到运维工程师自学之路 第七十九集 (基于Jenkins自动打包并部署Tomcat环境)2

紧接上文 4、新建Maven项目 clean package -Dmaven.test.skiptrue 用于构建项目并跳过执行测试 拉到最后选择构建后操作 SSH server webExec command scp 192.168.77.18:/root/.jenkins/workspace/probe/psi-probe-web/target/probe.war /usr/local/tomcat/webapps/ /usr/loca…

伦敦银和伦敦金的区别

伦敦银河伦敦金并称贵金属交易市场的双璧&#xff0c;一般投资贵金属的投资者其实不是交易伦敦金就是交易伦敦银。相信经过一段时间的学习和投资&#xff0c;不少投资者都能分辨二者的区别。下面我们就来谈谈伦敦银和伦敦金有什么异同&#xff0c;他们在投资上是否有差别。 交易…

股票预测和使用LSTM(长期-短期-记忆)的预测

一、说明 准确预测股市走势长期以来一直是投资者和交易员难以实现的目标。虽然多年来出现了无数的策略和模型&#xff0c;但有一种方法最近因其能够捕获历史数据中的复杂模式和依赖关系而获得了显着的关注&#xff1a;长短期记忆&#xff08;LSTM&#xff09;。利用深度学习的力…

Android初学之android studio运行java/kotlin程序

第一步骤&#xff1a;File—>New—>New Module&#xff0c;然后弹出一个框&#xff0c;&#xff08;左边&#xff09;选择Java or Kotlin Library&#xff0c;&#xff08;右边&#xff09;编辑自己的图书馆名、包名、类名&#xff0c;选择Java一个语言&#xff0c;然后F…

分享漂亮electerm主题

Electerm 字体建议设置为&#xff1a;Consolas 和 Microsoft YaHei UI 主题配置如下&#xff1a; themeNameNice main-dark#171717 main-light#2E3338 text#ddd text-light#fff text-dark#888 text-disabled#777 primary#CACACA info#FFD166 success#06D6A0 error#EF476F wa…

Unity shader 入门之渲染管线一、总览

如下示意图 应用阶段(ApplicationStage)&#xff1a;准备场景信息&#xff08;视景体&#xff0c;摄像机参数&#xff09;、粗粒度剔除、定义每个模型的渲染命令&#xff08;材质&#xff0c;shader&#xff09;——由开发者定义&#xff0c;不做讨论。几何阶段(GemetryStage)&…

星戈瑞分析FITC-PEG-Alkyne的荧光特性和光谱特性

​欢迎来到星戈瑞荧光stargraydye&#xff01;小编带您盘点&#xff1a; FITC-PEG-Alkyne的荧光特性和光谱特性是对其荧光性能进行分析的方面。以下是FITC-PEG-Alkyne的一些常见荧光特性和光谱特性&#xff1a; **1. 荧光激发波长&#xff1a;**FITC-PEG-Alkyne的荧光激发波长通…

【校招VIP】java语言考点之分代垃圾回收

考点介绍&#xff1a; JVM垃圾回收是面试里绕不开的考点&#xff0c;尤其是分代回收算法&#xff0c;集各种普通垃圾回收于一身&#xff0c;成为垃圾回收之王。但是也造成多个阶段的GC的不同&#xff0c;需要从对象的大小和使用频度等角度去考虑每个阶段的算法选择和造成的问题…

Docker 微服务实战

1. 通过IDEA新建一个普通微服务模块 1.1 建Module docker_boot 1.2 改写pom <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&…

OpenHarmony应用实现二维码扫码识别

本文转载自《OpenHarmony应用实现二维码扫码识别》&#xff0c;作者zhushangyuan_ 概念介绍 二维码的应用场景非常广泛&#xff0c;在购物应用中&#xff0c;消费者可以直接扫描商品二维码&#xff0c;浏览并购买产品&#xff0c;如图是购物应用的扫描二维码的页面。 本文就以橘…

TUME儿童毛毯上架亚马逊做CPC认证测试

毛毯(英文Blanket)&#xff0c;是一种常用的床上用品&#xff0c;具有保暖功能&#xff0c;与被子相比较薄。其原料多采用动物纤维&#xff08;如羊毛、马海毛、兔毛、羊绒、驼绒、牦牛绒&#xff09;或腈纶、粘胶纤维等化学纤维&#xff0c;也有的是动物纤维与化纤混纺制成的。…

软件产品需要做测评报告吗?

软件测试报告 毋庸置疑&#xff0c;当然需要&#xff0c;软件测试报告对软件测试过程中的评估、沟通、风险掌控、缺陷修复、发展方向等方面都有着非常重要的作用。接下来我们具体讲讲&#xff1a; 1、软件产品质量的客观评价 &#xff08;1&#xff09;发现软件产品存在的问题…

蓄电池管理,金融公司需警惕!

蓄电池在数据中心的UPS系统中作为备用电源&#xff0c;可以在电力中断时提供持续的电力供应&#xff0c;以保障数据中心的正常运行。 因此&#xff0c;蓄电池监控在数据中心行业具有重要意义。 客户案例 上海某金融服务公司拥有多个数据中心&#xff0c;为其核心业务提供支持。…