数据结构----AVL树

news2024/9/23 23:23:00

       小编会一直更新数据结构相关方面的知识,使用的语言是Java,但是其中的逻辑和思路并不影响,如果感兴趣可以关注合集。

       希望大家看完之后可以自己去手敲实现一遍,同时在最后我也列出一些基本和经典的题目,可以尝试做一下。大家也可以自己去力扣或者洛谷牛客这些网站自己去练习,数据结构光看不敲是学不好的,加油,祝你早日学会数据结构这门课程。

       有梦别怕苦,想赢别喊累。

 前言

        在之前学习二叉搜索树时,我们知道当一颗二叉搜索树的左右高度不平衡时,那这棵树的查询的时间复杂度可能达到O(n)。这显然不是我们想要的。

        其实在树中存在一种操作----旋转,这种操作的对象是节点,可以达到改变树结构的目的,因此我们可以借助旋转来把上面这颗左右高度不平衡的二叉搜索变得左右高度平衡一点。我们可以把根节点4向右旋转,同时把根节点3也向右旋转,那下面这棵树就变平衡了。

        在树形结构中规定如果一个节点的左右孩子高度差超过1,则此节点失衡,才需要旋转。

 概述

        在计算机科学中avl树又称平衡二叉搜索树,顾名思义就是可以让检索效率一直是O(logn)的一种树形数据结构。二叉搜索树在插入和删除时,节点可能失衡,因此如果在插入和删除时通过旋转,始终让二叉搜索树保持平衡,称为自平衡的二叉搜索树。另外AVL树是自平衡二叉搜索树的实现之一,后面我们要学习的红黑树也是自平衡二叉搜索树的实现之一。

 实现

    结构

        avl树的结构其实和二叉搜索树差不多,唯一区别就是需要判断每个节点是否平衡,因此我们多定义一个变量去维护当前节点高度,初始值为1,因为我们计算高度是从当前节点开始。

static class AVLNode {
        int key;
        int value;
        AVLNode left;
        AVLNode right;
        int height = 1; //高度

        public AVLNode(int key, int value) {
            this.key = key;
            this.value = value;
        }

        public AVLNode(int key) {
            this.key = key;
        }

        public AVLNode(int key, int value, AVLNode left, AVLNode right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }

        有的地方实现是维护两个变量,一个左子树高度,一个右子树高度,其实大差不差,能实现这两种的其中一种,另一种实现起来也是游刃有余。

        接着我们也需要提供几个工具方法,例如获取当前节点高度,更新节点高度,判断节点是否平衡这些的。

        获取当前节点高度就很简单的啦,我们有一个变量height是维护高度的,直接返回就好了。

    //求节点高度
    private int height(AVLNode node) {
        return node == null ? 0 : node.height;
    }

         更新节点高度,其实如果把之前题目做过的话,有一道二叉树的最大深度这道题其实和这个逻辑是一样的,求出左右子树高度最大值然后+1就好了。

        104. 二叉树的最大深度 - 力扣(LeetCode)

    //更新节点高度(新增,删除,旋转)
    private void updateHeight(AVLNode node) {
        node.height = Math.max(height(node.left), height(node.right)) + 1;
    }

        判断节点是否平衡,我们还需要引出一个概念就是平衡因子(balance factor 简称 bf),这玩意其实就是左子树 - 右子树的高度差,根据bf的值就可以判断左右子树是否平衡。

  

    // 判断该节点是否平衡
    // 平衡因子(balance factor) = 左子树高度 - 右子树高度
    // 返回 0,1,-1 平衡
    // >1 或者 <-1 不平衡
    private int bf(AVLNode node) {
        return height(node.left) - height(node.right);
    }

   失衡情况

        在实现avl树之前,我们还需要知道avl树的四种失衡情况,才能更好决定旋转的节点与方向。

        第一种情况,下面这就是一颗左高右低的不平衡二叉搜索树。

       

        我们可以将节点3向右旋转,那这时节点3就变成了根节点,但是节点3此时也有了2,4,5三个子节点,这当然是不行的,所以我们还需要将节点4的父节点改成节点5,那树的结构就变成了下面这样。

    第二种情况也是左高右低的不平衡二叉树,不过在子树结构上又有所不同。

        

     这时如果我们按照上面的处理方式将节点二右旋一次就得到了下面这样的一棵树

       这时这颗树的不平衡问题仍然没有解决,因此我们知道有的情况不能单靠一次右旋就能解决问题。

       其实前人已经帮我们总结好了二叉搜索树的四种失衡情况。

        第一种叫做左左(LL),看下面这棵树,失衡节点(节点5)的bf > 1,即左边更高,这是第一个左的含义,接着失衡节点的左孩子(节点3)的bf>=0 ,即左孩子也是左边更高或者等高,这是第二个左的含义。

       第二种叫做左右(LR),看下面这颗树,失衡节点(节点6)的bf > 1,即左边更高,这是第一个左的含义,失衡节点的左孩子(节点2)的bf < 0 即左孩子这边是右边更高,这是第二个右的含义。

       前两种情况理解了,其实剩下两种就是对称的。

       第三种叫做右左(RL),下面这颗树,失衡节点(节点2)的bf <  -1,即右边更高,这是第一个右的含义,失衡节点的右孩子(节点6)的bf > 0 ,即右孩子这边是左边更高,这是第二个左的含义。

        第四种叫做右右(RR),下面这颗树,失衡节点(节点2)的bf <  -1,即右边更高,这是第一个右的含义,失衡节点的右孩子(节点4)的bf <= 0 ,即右孩子这边是右边更高或者等高,这是第二个右的含义。

       了解完四种情况后,我们来看一下对于四种情况我们怎样旋转才可以恢复平衡,其实左左只需要左孩子节点向右旋转一次,就可以恢复平衡,与之对称右右只需要右孩子节点向左旋转一次就可以了,这都是两种比较简单的情况。接着对于左右和右左我们都需要两次旋转才可以恢复平衡。对于左右,我们先让失衡节点的左孩子先向左旋转就来到了下面这种情况,这种情况其实就是左左,我们再右旋一此就好了。右左也是一样的,先右子树右旋,就变成了右右的情况,然后左旋一次就可以恢复情况了。

   左旋

         其实左旋就两步,第一步是让原来的右子树节点上位到根节点,第二步就是把原来右子树的左子树变成原来根节点的左子树。另外不要忘了更新节点高度,其实在二叉搜索树旋转的过程中,只有根节点和旋转的孩子节点高度才会改变,更新高度的顺序不可以改变,这里可以思考一下为什么。

    /**
     * @Param:node 要旋转的节点
     * @Return: 新的根节点
     */
    private AVLNode leftRotate(AVLNode node) {
        AVLNode rightTree = node.right;
        // 左旋,右边肯定高,不用考虑rightTree为null
        AVLNode rightTreeLeft = rightTree.left;
        rightTree.left = node; // 上位根节点
        node.right = rightTreeLeft; // 换父节点
        // 更新高度
        updateHeight(node);
        updateHeight(rightTree);
        return rightTree;
    }

   右旋

        其实通过前面的左旋,我们也大概知道了右旋也是分两步,与左旋对称的两步。最后更新高度

    /**
     * @Param:node 要旋转的节点
     * @Return: 新的根节点
     */
    private AVLNode rightRotate(AVLNode node) {
        AVLNode leftTree = node.left;
        // 右旋,左边肯定高,不用考虑leftTree为null
        AVLNode leftTreeRight = leftTree.right;
        leftTree.right = node; // 上位根节点
        node.left = leftTreeRight; // 换父节点
        // 更新高度
        updateHeight(node);
        updateHeight(leftTree);
        return leftTree;
    }

    左右旋

        其实我们完成左旋转和右旋转后,这个方法很好实现,先调一次左旋再调一次右旋就好了,但是不要忘了左子树左旋完后把返回的根节点设置为根节点的左子树

    /**
     * 先左旋左子树,再右旋根节点
     * @param node
     * @Return: 新的根节点
     */
    private AVLNode leftRightRotate(AVLNode node) {
        node.left = leftRotate(node.left);
        return rightRotate(node);
    }

     右左旋

        同样不要忘了右子树右旋完后把返回的根节点设置为根节点的右子树

    /**
     * 先右旋右子树,再左旋根节点
     * @param node
     * @Return: 新的根节点
     */
    private AVLNode rightLeftRotate(AVLNode node) {
        node.right = rightRotate(node.right);
        return leftRotate(node);
    }

        到此四种失衡情况以及四种旋转情况的代码我们都已经完成了。 

  balance

        接着我们来实现一个工具方法,它的作用就是检查节点是否失衡并调整,如果失衡就重新平衡这棵树,然后返回根节点。如果没失衡我们也是返回根节点。

        首先判断节点是否失衡,我们只需要通过它的平衡因子就可以知道了,拿到了当前节点的平衡因子,如果它失衡,我们就对照四种失衡情况进行旋转方法的调用,这个balance方法实现起来也是轻轻又松松啊。

    /**
     * 平衡方法
     * @param node
     * @Return: 新的根节点
     */
    private AVLNode balance(AVLNode node) {
        if (node == null) {
            return null;
        }
        // 平衡因子
        int bf = bf(node);
        if (bf > 1 && bf(node.left) >= 0) { // LL
            return rightRotate(node);
        } else if (bf > 1 && bf(node.left) < 0) { // LR
            return leftRightRotate(node);
        } else if (bf < -1 && bf(node.right) > 0) { // RL
            return rightLeftRotate(node);
        } else if (bf < -1 && bf(node.right) <= 0) { // RR
            return leftRotate(node);
        }
        return node;
    }

  put

        接着我们来实现平衡二叉搜索树新增节点的方法,其实大体的逻辑和上一篇我们在二叉搜索树中插入节点是一样的,我们还是用递归去实现,唯一不同的就是,在节点插入后,我们要去更新高度,同时更新高度之后可能会导致失衡,所以我们还需要调用一下balance方法,就多了这两步,其它和二叉搜素树都是一样的。

    /**
     * 新增节点
     * @param key
     * @param value
     * @Return: void
     */
    public void put(int key, int value) {
        root = doPut(root, key, value);
    }

    private AVLNode doPut(AVLNode node, int key, int value) {
        // 1.找到空位,创建新节点
        if (node == null) {
            return new AVLNode(key, value);
        }
        // 2. key已经存在,更新
        if (key == node.value) {
            node.value = value;
            return node;
        }
        // 3.继续查找
        if (key < node.key) {
            node.left = doPut(node.left, key, value); // 向左
        } else {
            node.right = doPut(node.left, key, value); // 向右
        }
        // 更新高度
        updateHeight(node);
        // 调整
        return balance(node);
    }

 remove

        删除节点其实和二叉搜索树的逻辑也是一样的,不同的还是我们需要更新节点高度检查调整树的结构。关于删除逻辑有不清楚的可以去看我的上一篇关于二叉树的文章,那里有详细讲解。

    /**
     * 删除节点
     *
     * @param key
     * @Return: void
     */
    public void remove(int key) {
        root = doRemove(this.root, key);
    }

    private AVLNode doRemove(AVLNode node, int key) {
        // 1. node == null
        if (node == null) {
            return null;
        }
        // 2. 没找到key
        if (key < node.key) {
            node.left = doRemove(node.left, key);
        } else if (node.key < key) {
            node.right = doRemove(node.right, key);
        } else { // 3. 找到key
            if (node.left == null && node.right == null) { // 3.1 没有孩子
                return null;
            } else if (node.left == null) { // 3.2 只有一个孩子
                node = node.right;
            } else if (node.right == null) {
                node = node.left;
            } else {// 3.3 有两个孩子
                AVLNode s = node.right;
                while (s.left != null) {
                    s = s.left;
                }
                // s 后继节点
                s.right = doRemove(node.right, s.key);
                s.left = node.left;
                node = s;
            }
        }
        // 4. 更新高度
        updateHeight(node);
        // 5. 调整
        return balance(node);
    }

相关题目

1382. 将二叉搜索树变平衡 - 力扣(LeetCode)

       平衡二叉搜索树与二叉搜索树的区别就是树的结构是否平衡,从而实现查找效率达到O(logn),完成上面这一道题也差不多了,另外可以多做一些二叉搜索树的题目。

        如果不是天才,就请一步一步来。

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

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

相关文章

牛客网习题——通过C++实现

一、目标 实现下面4道练习题增强C代码能力。 1.求123...n_牛客题霸_牛客网 (nowcoder.com) 2.计算日期到天数转换_牛客题霸_牛客网 (nowcoder.com) 3.日期差值_牛客题霸_牛客网 (nowcoder.com) 4.打印日期_牛客题霸_牛客网 (nowcoder.com) 二、对目标的实现 1.求123...n_…

Java二十三种设计模式-访问者模式(21/23)

本文深入探讨了访问者模式&#xff0c;一种允许向对象结构添加新操作而不修改其本身的设计模式&#xff0c;涵盖了其定义、组成部分、实现方式、使用场景、优缺点、与其他模式的比较&#xff0c;以及最佳实践和替代方案。 访问者模式&#xff1a;为对象结构添加新的操作 引言 …

黑神话:悟空-配置推荐

显卡推荐&#xff08;按类别整理&#xff09; 1. GTX 10系列、GTX 16系列&#xff1a; 如果希望体验光线追踪&#xff0c;建议根据预算升级到RTX 40系列显卡。对于1080p分辨率&#xff0c;至少需要RTX 4060才能流畅运行。 2. RTX 20系列&#xff1a; RTX 2060、RTX 2070&#…

Openboxes 移动终端APP项目开发环境搭建与调试

文章目录 前言项目简介APP开发环境搭建APP开发环境启动及调试主应用程序启动及调试结语 前言 openboxes 项目还有一个针对移动端的项目&#xff1a;openboxes-mobile&#xff0c;但是这个项目的默认分支&#xff08;develop&#xff09;并没有与openboxes的默认分支对应&#…

LabVIEW优化内存使用

在LabVIEW中&#xff0c;优化内存使用的关键在于理解LabVIEW的内存管理机制并采用一些最佳实践。以下是一些可能帮助减少内存占用的方法&#xff1a; 1. 减少数据副本的生成 避免不必要的数据复制&#xff1a;每当你在程序中传递数组或子数组时&#xff0c;LabVIEW可能会创建副…

充电宝哪些品牌的性价比是最高的?开学最推荐入手四款充电宝

随着新学期的到来&#xff0c;学生们对充电宝的需求愈发迫切。无论是在校园内上课、图书馆自习&#xff0c;还是在外出游玩时&#xff0c;充电宝都成为了我们必不可少的随身装备。然而&#xff0c;市场上充斥着各种品牌和型号&#xff0c;如何选择一款性价比高的充电宝&#xf…

React+Vis.js(04):设置节点显示图片

文章目录 实现效果关键代码完整代码设置图片边框和背景颜色我们继续以 复仇者联盟为例,来介绍如何实现节点显示 图片。 实现效果 以图片进行节点的显示,使得显示效果更加直观,信息更为明了。 关键代码 在vis.js中,通过属性shape来控制节点显示为图像。 const nodes …

linux | 苹果OpenCL(提高应用软件如游戏、娱乐以及科研和医疗软件的运行速度和响应)

点击上方"蓝字"关注我们 01、引言 >>> OpenCL 1.0 于 2008 年 11 月发布。 OpenCL 是为个人电脑、服务器、移动设备以及嵌入式设备的多核系统提供并行编程开发的底层 API。OpenCL 的编程语言类似于 C 语言。其可以用于包含 CPU、GPU 以及来自主流制造商如 …

关于Hipe并发库中动态线程库DynamicThreadPond的一点解读(二)

文章目录 前提动态减少代码解读 动态增加线程池退出时发生了什么&#xff1f;总结附录 前提 我们在关于Hipe并发库中动态线程库DynamicThreadPond的一点解读(一)中介绍了DynamicThreadPond如何初始化&#xff0c;如何向任务队列中添加任务&#xff0c;线程池中的线程如何执行任…

机器学习——第十二章 计算学习理论

目录 12.1 基础知识 12.2 PAC学习 12.3 有限假设空间 12.3.1 可分情形 12.3.2 不可分情形 12.4 VC维 12.5 Rademacher复杂度 12.6 稳定性 12.1 基础知识 计算学习理论(computational learning theory)研究的是关于通过"计算"来进行"学习"的理论…

SAP无参考收货

其他收货 如果我们未参考其他凭证&#xff08;采购订单、生产订单或预留&#xff09;输入货物移动&#xff0c;则我们将讲到其他收货。因为在实际过帐之前&#xff0c;系统不会存储任何物料、数量、交货日期、接收工厂或来源方面的信息&#xff0c;所以此类收货属于计划外货物…

一文入门re 正则表达式

一、常用方法 &#xff08;一&#xff09;匹配 一般使用方法 第一个参数&#xff1a;正则模式 第二个参数&#xff1a;需要处理的字符串 第三个参数&#xff1a;附加处理方法result从任意位置开始匹配&#xff0c;返回match&#xff0c;没有匹配到返回None result re.searc…

MiniCPM-V: A GPT-4V Level MLLM on Your Phone论文阅读

大模型的趋势&#xff1a;模型性能越来越好&#xff0c;模型参数变小&#xff0c;端边设备计算能力变强。 MiniCPM-V优点 结果好、OCR能力突出、多分辨率、多语言、易于部署 模型结构 图片encoder适用vit。输入整体以及切片。切片使用自适应算法&#xff0c;通过计算分数&am…

揭秘图数据库:如何颠覆社交网络分析的游戏规则

在当今信息化时代&#xff0c;社交网络的数据量以指数级的速度增长&#xff0c;人们在社交网络中产生了海量的交互数据。如何从这些复杂的数据中提取有价值的信息&#xff0c;成为了数据科学和社交网络分析领域的一大挑战。图数据库作为一种有效的数据管理和分析工具&#xff0…

Anzo Capital视角下马丁格尔交易策略的利与弊

在Anzo Capital&#xff0c;我们深入探讨了马丁格尔交易策略的多面性。这种策略因其简单性而受到一些交易者的欢迎&#xff0c;但同时也存在着不可忽视的风险。本文将客观分析其优势和潜在缺陷&#xff0c;帮助投资者在Anzo Capital平台上做出更明智的决策。 马丁格尔策略的核…

JavaEE项目总结(1)

一、在vue项目中安装axios 由于需要使用axios框架进行异步请求&#xff0c;所以需要在vue项目中安装axios框架。在官方下载速度较慢&#xff0c;所以选择更换镜像源&#xff08;我使用的是华为云镜像&#xff09; 在项目终端中输入npm config set registry http://mirrors.…

MES系统:制造业转型升级的驱动力与效益源泉

制造业之所以需要并采纳MES系统&#xff08;Manufacturing Execution System&#xff0c;制造执行系统&#xff09;&#xff0c;主要是出于以下几个方面的考虑和需求。MES系统能够为企业带来显著的好处&#xff0c;具体体现在以下几个方面&#xff1a; 制造业需要MES系统的原因…

信贷风控架构一张图

记得刚入门风控时,由于做的模型岗,总有一种不能窥探风控全貌的疑惑。随着经验的积累,以及一些岗位的体验,慢慢对风控有了更清晰的认知。本文以一张图的形式简单地呈现信贷风控架构,让你能快速窥探信贷风控全貌。 文章目录 一、什么是风险管控?二、信贷风控架构一张图三、…

PCDN日常应用--公共资源利用

1、前言 PCDN 是通过在 CDN 的边缘节点上部署代理服务器&#xff0c;来实现主动调度和传输内容的方法。当用户请求内容时&#xff0c;PCDN 将根据各个节点的负载情况、距离、传输速度等一系列因素来动态选择最优的节点来提供内容。这样的调度方式使得 PCDN 在面对大规模请求时能…

健康管理系统解决方案

产品简介 脉购CRM健康管理系统是一款集会员健康管理以及会员数字化营销于一体的系统解决方案&#xff0c;旨在帮助企业更好地提供个性化的健康服务&#xff0c;维护好会员关系&#xff0c;通过有效的营销手段增加健康会员粘性和满意度&#xff0c;最终达到业绩增长的目的。 核…