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,在服务器端按需编译返回,完全跳过了打包这个…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...

linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...
站群服务器的应用场景都有哪些?
站群服务器主要是为了多个网站的托管和管理所设计的,可以通过集中管理和高效资源的分配,来支持多个独立的网站同时运行,让每一个网站都可以分配到独立的IP地址,避免出现IP关联的风险,用户还可以通过控制面板进行管理功…...
Linux系统部署KES
1、安装准备 1.版本说明V008R006C009B0014 V008:是version产品的大版本。 R006:是release产品特性版本。 C009:是通用版 B0014:是build开发过程中的构建版本2.硬件要求 #安全版和企业版 内存:1GB 以上 硬盘…...