JavaDS —— AVL树
前言
本文章将介绍 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 树,记得测试自己的代码有没有错误,这里要测试两个东西,第一个是中序遍历的时候是否有序(检测是否为二叉搜索树),第二个东西就是平衡因子有没有错误以及是否是一个高度平衡的二叉树(因为都与高度有关,所以可以放在同一个方法里去写测试代码)。
性能分析
AVL 树始终都能保持高度的平衡(即左右子树的高度差<= 1),所以在搜索数据的时候,时间复杂度为O(log N),举个例子:加上有10亿个数据,需要在其中找到一个数据,如果使用 AVL 树,2的30次方等于1,073,741,824,所以最多只需要查找30次就能找到目标值,可见AVL 树在查找中的优秀表现。
由于AVL树在插入和删除的时候,会进行旋转调整,这也就意味着大量时间和资源的消耗,所以AVL 树比较适合静态数据的查找,如果数据涉及大量的删除或者插入就会导致AVL 树的效率下降,这也就是为什么现在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;}
}相关文章:
JavaDS —— AVL树
前言 本文章将介绍 AVL 树的概念,重点介绍AVL 树的插入代码是如何实现的,如果大家对 AVL 树的删除(还是和二叉搜索树一样使用的是替换删除法,然后需要判断是否进行旋转调整)感兴趣的话,可以自行去翻阅其他…...
NSSCTF练习记录:[SWPUCTF 2021 新生赛]jicao
题目: 这段PHP代码的意思是: 对index.php文件进行语法高亮显示,插入flag.php文件,变量id的值为POST传递的值,变量json的值为GET传递的json类型的值。当id值为wllmNB且json中含有键为“x”,值为“wllm”的时…...
LabVIEW位移检测系统
工业控制器的位移检测在保证机械设备精确运行中发挥着重要的作用。开发了一种基于LabVIEW的高精度位移检测系统,该系统通过集成硬件与软件的优化配置,实现了对工业控制器位移的精确测量和分析。 项目背景 在传统工业生产中,位移检测系统往往…...
02、MySQL-DML(数据操作语言)
目录 1、添加数据(INSERT) 2、修改数据(UPDATE) 3、删除数据(DELETE) 1、添加数据(INSERT) 注意: 插入数据时,指定的字段顺序需要与值的顺序是一一对应的字符串和日期型数据应该包含在引号中插入的数据大小,应该在字段的规定范围内 给指定…...
vue3 项目部署到线上环境,初始进入系统,页面卡顿大概一分钟左右,本地正常无卡顿。localStorage缓存1MB数据导致页面卡顿。
使用vue3进行项目开发,前端框架使用jeecg-boot进行开发,项目初期,打包部署到生产环境,无异常。某天,进行前端项目打包部署到生产环境,突然出现异常情况,部署到线上环境,初始进入系统…...
软件更新中的风险识别与质量保证机制分析
您好,我是程序员小羊! “微软蓝屏”事件暴露了网络安全哪些问题? 近日,一次由微软视窗系统软件更新引发的全球性“微软蓝屏”事件,不仅成为科技领域的热点新闻,更是一次对全球IT基础设施韧性与安全性…...
QT下载与安装
我们要下载开源的QT,方式下载方式: 官网 登录地址:http://www.qt.io.com 点击右上角的Download. Try.按钮;进入一下画面: 如果进入的是以下画面: 直接修改网址:www.qt.io/download-dev; 改为w…...
Java 2.2 - Java 集合
Java 集合,也叫做容器,主要是由两大接口派生而来:一个是 Collection 接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于 Collection 接口,其下又有三个主要的子接口&#…...
Linux驱动.之I2C,iic驱动层(二)
一、 Linux下IIC驱动架构 本篇只分析,一个整体框架。 1、首先说说,单片机,的i2c硬件接口图,一个i2c接口,通过sda和scl总线,外接了多个设备device,通过单片机,来控制i2c的信号发生&…...
【STM32】USART串口和I2C通信
个人主页~ USART串口和I2C通信 USART串口一、串口1、简介2、电路要求3、参数及时序 二、USART外设1、USART结构2、波特率发生器 三、数据包1、HEX数据包HEX数据包接收 2、文本数据包文本数据包接收 I2C通信一、简介二、通信协议1、硬件电路2、I2C时序基本单元 三、I2C外设1、简…...
【Material-UI】按钮组:垂直按钮组详解
文章目录 一、按钮组概述1. 组件介绍2. 基本用法 二、垂直按钮组的应用场景1. 导航菜单2. 表单操作3. 选项切换 三、按钮组的样式定制1. 变体(Variants)2. 颜色(Colors) 四、垂直按钮组的优势1. 空间利用2. 可读性与易用性3. 视觉…...
DDR5 的优势与应用
DDR5 是新一代 DRAM 内存,具有一系列强大的功能,可提升可靠性、可用性和可维护性 (RAS),降低能耗并显著提高性能。请查看下方表格,了解 DDR4 和 DDR5 之间的一些主要特性差异。 DDR5 的优势 特性/选项 DDR4DDR5DDR5 优势数据速率…...
STM32 - 笔记
1 STM32的串口通信 【keysking的STM32教程】 第8集 STM32的串口通信_哔哩哔哩_bilibili 波特律动 串口助手...
基于QT实现的简易WPS(已开源)
一、开发工具及开源地址: 开发工具:QTCreator ,QT 5 开源地址: GitHub - Whale-xh/WPS_official: Simple WPS based on QTSimple WPS based on QT. Contribute to Whale-xh/WPS_official development by creating an acc…...
Flask-WTF 表单处理详细教程(第六阶段)
目录 Flask-WTF 表单处理详细教程1. 安装 Flask-WTF2. 创建 Flask 应用3. 创建表单类表单字段解释: 4. 渲染表单渲染模板CSRF 保护 5. 表单验证6. 处理错误7. 完整示例8. 结论 Flask-WTF 表单处理详细教程 Flask-WTF 是 Flask 框架的一个扩展,简化了 We…...
C语言 | Leetcode C语言题解之第330题按要求补齐数组
题目: 题解: int minPatches(int* nums, int numsSize, int n) {int patches 0;long long x 1;int index 0;while (x < n) {if (index < numsSize && nums[index] < x) {x nums[index];index;} else {x << 1;patches;}}retu…...
无人机之测绘行业篇
无人机倾斜摄影技术凭借快速高效、机动灵活、成本低等优势,正慢慢颠覆传统测绘的作业方式,已成为测绘行业的“新宠”,将倾斜摄影技术应用到无人机上,实际上就是在做一个三维模型,而建立起来的这个模型更加真实…...
Java编程:每日挑战
目录 题目1.一个抽象类并不需要其中所有的方法都是抽象的。( )2.下面有关java hashmap的说法错误的是?A. HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。B. HashMap 的实现不是同步的,意味着它不…...
【自动驾驶】ubuntu server安装桌面版
目录 安装桌面版当锁屏界面使用root用户登录错误时 这里环境一开始是ubuntu20.04服务器版本 安装桌面版 sudo apt-get update sudo apt-get upgrade apt-get install -y ubuntu-desktop # 如果你不想安装一些附加的程序,可用以下命令 sudo apt install --no-instal…...
前端模块化-手写mini-vite
前言 本文总结了一些关于 Vite 的工作原理,以及一些实现细节。 本节对应的 demo 可以在这里找到。 什么是 Vite Vite 是一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个…...
Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...
docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...
Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)
设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile,新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...
Docker拉取MySQL后数据库连接失败的解决方案
在使用Docker部署MySQL时,拉取并启动容器后,有时可能会遇到数据库连接失败的问题。这种问题可能由多种原因导致,包括配置错误、网络设置问题、权限问题等。本文将分析可能的原因,并提供解决方案。 一、确认MySQL容器的运行状态 …...
车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...
验证redis数据结构
一、功能验证 1.验证redis的数据结构(如字符串、列表、哈希、集合、有序集合等)是否按照预期工作。 2、常见的数据结构验证方法: ①字符串(string) 测试基本操作 set、get、incr、decr 验证字符串的长度和内容是否正…...
