数据结构与算法 - AVL树

news2024/11/27 6:19:12

一、概述

1. 历史

AVL树是一种自平衡二叉搜索树,由托尔·哈斯特罗姆在1960年提出并在1962年发表。它的名字来源于发明者的名字:Adelson-Velsky和Landis,他们是苏联数学家,于1962年发表了一篇论文,详细介绍了AVL树的概念和性质。

在二叉搜索树中,如果插入的元素按照特定的顺序排列,可能会导致树变得非常不平衡,从而降低搜索、插入和删除的效率。为了解决这个问题,AVL树通过在每个节点中维护一个平衡因子来确保树的平衡。平衡因子是左子树的高度减去右子树的高度。如果平衡因子的绝对值大于等于2,则通过旋转操作来重新平衡树。

AVL树是用于存储有序数据的一种重要数据结构,它是二叉搜索树的一种改进和扩展。它不仅能提高搜索、插入和删除操作的效率,而且还能够确保树的深度始终保持在O(log n)的水平。随着计算机技术的不断发展,AVL树已经成为了许多高效算法和系统中必不可少的一种基础数据结构。

前面介绍过,如果一棵二叉搜索树长的不平衡,那么查询的效率会受到影响,如下图

通过旋转可以让树重新变得平衡,并且不会改变二叉搜索树的性质(即左边仍然小,右边仍然大)

2. 如何判断失衡

如果一个节点的左右孩子,高度差超过1,则此节点失衡,才需要旋转。

3. 处理高度

如何得到节点高度?一种方式之前做过的一道题目:求二叉树的最大深度(高度),但由于求高度是一种非常频繁的操作,因此将高度作为节点的一个属性,将来新增或删除时及时更新,默认为1

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

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

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

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

求高度

这里加入了height函数方便求节点为null时的高度

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

更新高度

将来新增、删除、旋转时,高度都可能发生变化,需要更新。

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

4. 何时触发失衡判断

定义平衡因子(balance factor)如下

平衡因子 = 左子树高度 - 右子树高度

当平衡因子

  • bf = 0, 1, -1时,表示左右平衡
  • bf > 1时,表示左边太高
  • bf < -1时,表示右边太高
    /**
     * 衡因子 balance factor = 左子树高度 - 右子树高度
     * bf = 0, 1, -1时,表示左右平衡
     * bf > 1时,表示左边太高
     * bf < -1时,表示右边太高
     * @param node
     * @return
     */
    private int bf(AVLNode node) {
        return height(node.left) - height(node.right);
    }

当插入新节点时,或删除节点时,引起高度变化时,例如

目前此树平衡,当再插入一个4时,节点们的高度都产生了相应的变化,8节点失衡了

再比如说,下面这棵树一开始也是平衡的

当删除节点8时,节点们的高度都产生了相应的变化,6节点失衡了

5. 失衡的四种情况

LL

  • 失衡节点的bf > 1,即左边更高
  • 失衡节点的左孩子bf >= 0,即左孩子这边也是左边更高或等高

LR

  • 失衡节点的bf > 1,即左边更高
  • 失衡节点的左孩子的 bf < 0,即左孩子这边是右边更高

RL

  • 失衡节点的 bf < -1,即右边更高
  • 失衡节点的右孩子的bf > 0,即右孩子这边左边更高

RR

  • 失衡节点的bf < -1,即右边更高
  • 失衡节点的右孩子的 bf <= 0,即右孩子这边右边更高或等高

二、实现

1. 解决失衡

失衡可以通过树的旋转解决。什么是树的旋转呢?它是在不干扰元素顺序的情况下更改结构,通常用来让树的高度变得平衡。

观察下面一棵二叉搜索树,可以看到,旋转后,并未改变树的左小右大特性,但根、父、孩子节点都发生了变化

LL - 右旋

旋转前

  • 红色节点,旧根(失衡节点)
  • 黄色节点,旧根的左孩子,将来作为新根,旧根是它右孩子
  • 绿色节点,新根的右孩子,将来要换爹作为旧根的左孩子

旋转后

代码:

    /**
     * 右旋
     * @param red 要旋转的节点(失衡)
     * @return 新的根节点
     */
    private AVLNode rightRotate(AVLNode red) {
        AVLNode yellow = red.left;
        // AVLNode green = yellow.right;
        yellow.right = red;  // 上位
        red.left = yellow.right;  // 换爹

        // 更新节点高度
        updateHeight(red);
        updateHeight(yellow);

        return yellow;
    }

RR - 左旋 

旋转前

  • 红色节点,旧根(失衡节点)
  • 黄色节点,旧根的右孩子,将来作为新根,旧根是它左孩子
  • 绿色节点,新根的左孩子,将来要换爹作为旧根的右孩子

旋转后

代码:

    /**
     * 左旋
     * @param red 要旋转的节点(失衡)
     * @return 新的根节点
     */
    private AVLNode leftRotate(AVLNode red) {
        AVLNode yellow = red.right;
        red.right = yellow.left;
        yellow.left = red;

        // 更新节点高度
        updateHeight(red);
        updateHeight(yellow);

        return yellow;
    }

LR - 左右旋

指先旋转左子树,再右旋根节点(失衡),这时一次旋转并不能解决失衡

左子树旋转后 - 左旋

根右旋前

根右旋后

代码:

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

RL - 右左旋

指先右旋右子树,再左旋根节点(失衡)

右子树右旋后

根左旋前

根左旋后

代码:

    /**
     * 右左旋
     * 先右旋右子树,再左旋根节点
     * @param root
     * @return
     */
    private AVLNode rightLeftRotate(AVLNode root) {
        root.right = rightRotate(root.right);
        return leftRightRotate(root);
    }

判断及调整平衡

    /**
     * 判断及调整平衡代码
     * @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;
    }

以上四种旋转代码里,都需要更新高度,需要更新的节点是红色、黄色,而绿色节点高度不变

2. 新增

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

    private AVLNode doPut(AVLNode node, int key, Object value) {
        // 1. 找到空位,创建新结点返回
        if(node == null) {
            return new AVLNode(key, value);
        }

        // 2. key已有,更新
        if(key == node.key) {
            node.value = value;
            return node;
        }
        // 3. 继续查找
        if(key < node.key) {
            node.left = doPut(node.left, key, value);
        } else {
            node.right = doPut(node.right, key, value);
        }
        // 更新节点高度
        updateHeight(node);
        // 重新调整二叉搜索树
        return balance(node);
    }

3. 删除

    /**
     * 删除节点
     * @param key
     */
    public void remove(int key) {
        root = doRemove(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 1)没有孩子节点 2)只有一个孩子 3)有两个孩子
            if(node.left == null && node.right == null) {
                // 情况1 没有孩子节点
                return null;
            } else if(node.left == null) {
                // 情况2 只有右孩子
                node = node.right;
            } else if(node.right == null) {
                // 情况3 只有左孩子
                node = node.left;
            } else {
                // 情况4 有两个孩子
                AVLNode s = node.right;
                while(s.left != null) {
                    s = s.left;
                }
                s.right = doRemove(node.right, s.key);
                s.left = node.left;
                node = s;
            }
        }
        
        // 4. 更新高度
        updateHeight(node);
        // 5. 检查是否失衡
        return balance(node);
    }

4. 查询

    /**
     * 根据key查询节点的value值
     * @param key
     * @return
     */
    public Object get(int key) {
        return doGet(root, key);
    }

    private Object doGet(AVLNode node, int key) {
        if(node == null) {
            return null;
        }
        if(key < node.key) {
            return doGet(node.left, key);
        } else if(node.key < key) {
            return doGet(node.right, key);
        } else {
            return node.value;
        }
    }

5. 小结

AVL树的优点:

  • AVL树是一种自平衡树,保证了树的高度平衡,从而保证了树的查询和插入操作的时间复杂度均为O(log n)
  • 相比于一般二叉搜索树,AVL树对查询效率的提升更为显著,因为其左右子树高度的差值不会超过1,避免了二叉搜索树退化为链表的情况,使得整棵树的高度更低
  • AVL树的删除操作比较简单,只需要像插入一样旋转即可,在旋转过程中树的平衡性可以得到维护

AVL树的缺点:

  • AVL树每次插入和删除节点时可能需要进行旋转操作,这个操作比较耗时,因此在一些应用中不太适用
  • 在AVL树进行插入或删除操作时,为保持树的平衡需要不断进行旋转操作,在一些高并发环节和大数据量环境下,这可能会导致多余的写锁导致性能瓶颈
  • AVL树的旋转操作相对较多,因此在一些应用中可能会造成较大的空间浪费。

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

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

相关文章

插画用什么软件?11款不可低估的绘图工具推荐

现如今插画已经成为设计师的必备武器&#xff0c;而你是否被插画困住过设计进度呢&#xff1f;今天。即时设计设计师就来给大家来个“投机取巧”的好方法&#xff0c;特意为大家整理了11款绘制插画软件&#xff0c;尤其是第一款更是拥有3000插画素材&#xff0c;不仅省时省力&a…

前端day3-表格

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>day3-表格</title> </head> <body&g…

社交媒体分享预览图片和内容修改

在facebook发帖分享链接时&#xff0c;设置预览图片和内容 设置预览图片和内容 <head> <meta name"description" content"我是内容" /> </head> <body><img src"./1.jpg" alt"SEO Image" style"dis…

MapStruct丝滑替换BeanUtils.copyProperties

文章目录 1 工具应用场景2 使用效果3 使用方法3.1 引入jar包3.2 指定lombok与MapStruct执行顺序3.3 定义Mapper接口3.4 重新打包 4 练习手段 1 工具应用场景 BeanUtils.copyProperties拷贝对象同名属性&#xff0c;利用反射原理&#xff0c;耗时长&#xff0c;在对性能要求高的…

SEO专家教你玩转谷歌外链!

​要想玩转谷歌外链&#xff0c;你得了解并掌握以下三种主流的外链策略。每种策略在不同情况下都有独到之处&#xff0c;具体怎么做&#xff0c;我来详细说说。 1.GPB独立站外链。这种方式主要是在数量较少的独立域名上发布全是dofollow的链接。关键在于确保链接的高质量和持久…

清洁能源时代,ARMxy智慧网关为风电行业保驾护航

在全球能源转型的背景下&#xff0c;清洁能源的发展成为了各国关注的焦点。风电作为一种可再生能源&#xff0c;具有巨大的发展潜力。而 ARMxy 工控机作为工业自动化领域的重要设备&#xff0c;在风电行业中发挥着至关重要的作用&#xff0c;为清洁能源的发展提供了有力的支持。…

角色模块开发

文章目录 &#x1f31e; Sun Frame&#xff1a;SpringBoot 的轻量级开发框架&#xff08;个人开源项目推荐&#xff09;&#x1f31f; 亮点功能&#x1f4e6; spring cloud模块概览常用工具 &#x1f517; 更多信息1.easycode生成基础代码1.配置2.将dao层代码剪切到mapper层3.A…

设计师的救星,效率插件大合集!有了它设计效率翻倍

作为一名设计师&#xff0c;我相信大家都有这样的痛苦经历&#xff1a;为了完成一个设计项目&#xff0c;得下载一堆工具&#xff0c;像 snipaste、pureref、eagle 等&#xff0c;每个都要单独下载、单独打开使用。还有一些效率工具&#xff0c;比如 utools 或 hapigo&#xff…

OpenCV图像滤波(6)高斯滤波函数GaussianBlur()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 函数使用高斯滤波器对图像进行模糊处理。 该函数使用指定的高斯核对源图像进行卷积。支持原位过滤。 高斯模糊是一种有效的图像平滑技术&#xf…

实验23.硬盘分区,并编写硬盘驱动程序

已完成实验 已完成实验链接 简介 实验 23. 硬盘分区&#xff0c;并编写硬盘驱动程序 总结 创建硬盘并分区 加载硬盘分区 实现 printk 使能硬盘中断 interrupt.c idel 线程,主线程阻塞后此线程保底 thread.c sleep 函数 timer.c 加载分区信息 ide.c 创建硬盘 fdisk …

[Web服务器] 简易静态Web服务器的搭建

什么是Web服务器 可以为发出请求的浏览器提供静态文档的程序。 平时我们浏览百度新闻数据的时候&#xff0c;每天的新闻数据都会发生变化&#xff0c;那访问的这个页面就是动态的&#xff0c;而我们开发的是静态的&#xff0c;每天访问我们自己的静态web服务器&#xff0c;页面…

宠物托运网站如何搭建?5步帮你吸引70%顾客!

引言 随着人们生活水平的提高和宠物文化的普及&#xff0c;越来越多的宠物主人开始选择带着自己的宠物一同旅行或搬迁至新的城市。这一趋势催生了宠物托运服务行业的快速发展&#xff0c;而一个专业、高效的宠物托运网站则成为了连接宠物主人与托运服务提供商之间不可或缺的桥…

七夕情人节送什么礼物?四款好物分享,情侣必看!

在浪漫的七夕情人节&#xff0c;为心爱的人挑选一份特别的礼物是每对情侣表达爱意的重要方式。市场上琳琅满目的选择让人眼花缭乱&#xff0c;究竟什么样的礼物能触动TA的心弦&#xff1f;本篇分享将为您精选四款既实用又充满情意的好物&#xff0c;无论是甜蜜的开始还是长久的…

【区块链+医疗健康】医链 - 区块链医疗信息管理系统 | FISCO BCOS应用案例

根据《“十四五”规划和 2035 远景目标纲要》&#xff0c;我国在“十四五”时期将全面推进医疗信息化建设。工信部等部 门联合发布《关于加快推动区块链技术应用和产业发展的指导意见》&#xff0c;促进区块链在医疗健康等公共服务领域开 展应用&#xff0c;促进业务协同办理。…

1. dact-admin 中如何发布一个表并添加到导航栏

1.页面脚手架中根据自己需求选择生成的文件例如模型、控制器、翻译文件、数据库迁移文件等 访问 http://【你的域名】/admin/helpers/scaffold 例如&#xff1a; http://localhost:8000/admin/helpers/scaffold 2. 新建好之后来到路由文件 路径随便写&#xff0c;当然按照惯…

[C++] 模板进阶:特化与编译链接全解析

文章目录 非类型模板类型形参非类型模板参数代码示例 **模板的特化**为什么要有模板的特化函数模板特化使用场景与示例函数模板特化的实现细节 类模板特化全特化示例 偏特化部分优化通过进一步限制模板参数进行特化偏特化为指针类型示例&#xff1a;偏特化为引用类型示例&#…

红酒与午后:悠闲时光的惬意选择

阳光透过窗棂&#xff0c;轻轻洒在木质的茶几上&#xff0c;斑驳的光影交织出一幅静谧而温暖的画面。在这宁静的午后&#xff0c;一瓶洒派红酒&#xff08;Bold & Generous&#xff09;静静地摆放在那里&#xff0c;仿佛正等待着与你一同开启一段悠闲的品酒时光。 一、午后…

【往届均已完成EI、SCOPUS检索】第四届电气工程与计算机技术国际学术会议(ICEECT 2024,9月27-29)

第四届电气工程与计算机技术国际学术会议&#xff08;ICEECT2024&#xff09;将于9月27日-29日在哈尔滨举办。 会议主要围绕"电路与系统"、“电气工程材料”、“计算机视觉”、“计算机技术”等专业研究领域展开讨论。旨在为气工程、计算机技术等领域的专家学者及企业…

Figma汉化教程

Figma汉化教程&#xff0c;需要 5 步 第一步&#xff1a;我们打开一个Figma中文社区网站 https://www.figma.cool/cn。我们点击左上角的Figma汉化&#xff0c;进入Figma汉化安装的页面。 第二步&#xff1a;在Figma 软件汉化页面中&#xff0c;选择谷歌浏览器汉化&#xff0c;点…

Litestar GET function blocks OpenAI

题意&#xff1a;Litestar GET 函数阻塞 OpenAI 问题背景&#xff1a; When I transfer function to litestar, it suddenly stops OpenAI from returning a completion. I can print to console every declared variable except answer: 当我将函数传递给 litestar 时&#…