手撕AVL_二叉平衡树(图文并茂)

目录

前言

一 . AVL树的概念

二 . AVL树节点的定义

三 . AVL树的插入

1.插入节点

2.调节负载因子

四 . AVL树的旋转

1.左单旋

2.左右双旋

五 . AVL树性能分析

总结


前言

大家好,今天带大加手撕AVL树的插入


一 . AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。 一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

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

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(log2n)搜索时间复杂度O(log2n)


二 . AVL树节点的定义

为了AVL树实现简单,AVL树节点在定义时维护一个平衡因子,具体节点定义如下:

static class TreeNode{int val;int bf; // 平衡因子 -> 当前节点的平衡因子=右子树高度-左子树的高度TreeNode left;// 节点的左孩子TreeNode right;// 节点的右孩子TreeNode parent;// 节点的双亲public TreeNode(int val){this.val = val;}
}

注意: 当前节点的平衡因子=右子树高度-左子树的高度。但是,不是每棵树,都必须有平衡因子,这只是其中的一种实现 方式。


三 . AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可 以分为两步:

1.二分查找树的形式进行插入
while(){1.1 当前节点的val小于待插入节点的val -> 往右边迭代1.2 当前节点的val大于待插入节点的val -> 往左边迭代1.3 当前节点的val等于待插入节点的val -> 不允许插入}1.4 正式插入节点2.调节平衡因子2.1 如果当前节点是父节点的左节点 parent.bf++2.2 如果当前节点是父节点的右节点 parent.bf--;2.3 根据parent.bf判断是否需要继续向上调整2.3.1 如果parent.bf == 0 表示当前树平衡2.3.2 如果parent.bf == (1 || -1) 当前子树平衡,子树上面的情况未知,主要向上进行调整2.3.3 如果parent.bf == (2 || -2) 需要进行旋转,根据不同的情况,采取不同的旋转策略

1.插入节点

本来还想偷个懒的,发现二叉搜索树没有写,这就没办法了,一步一步来吧

假如我们需要在下面avl树中插入节点10

parent: 表示当前遍历到的节点的父节点 初始值为null

child: 表示当前遍历到的节点 初始值为root 即 5号节点

1.4 正式插入节点

给出部分代码

        TreeNode parent = null;TreeNode child = root;// 1.二分查找树的插入while(child != null){if(child.val > val){ //// 往左边插入parent = child;child = child.left;}else if(child.val < val){// 往右边插入parent = child;child= child.right;}else{// 树中已经存在该节点,不允许插入return false;}}// 节点正式插入if(parent.val > val){parent.left = node;}else{parent.right = node;}node.parent = parent;child = node;

至此第一步完美结束.我们开始第二大步调节平衡因子

2.调节负载因子

负载因子的调节和二叉树的旋转可以说是平衡树的核心了,但是只要理解透了,就是随便写,毫无难度!

  • 2.1 如果当前节点是父节点的左节点 parent.bf++
  • 2.2 如果当前节点是父节点的右节点 parent.bf--;

此时如果我们插入的元素使10,那parent.bf == 1

2.3.2 如果parent.bf == (1 || -1) 当前子树平衡,子树上面的情况未知,需要向上进行调整

以9为根节点的子树确实是平衡了,但是上面的子树我们并不清楚,可能平衡也可能不平衡,上面的两个都是不平衡的,我们来看一个平衡的例子

上面的意思是当前子树平衡,子树上面的情况未知,需要向上进行调整我们只知道以parent为根节点的子树的情况,并不清楚上面的情况

再次回到上面插入10的例子中,根据2.3.2可知需要向上调整

2.3.3 如果parent.bf == (2 || -2) 需要进行旋转,根据不同的情况,采取不同的旋转策略

到这就是开始旋转了

旋转一共分为四种,表示四种不同的情况,我先把这四种旋转应用的情况列举出来,具体如何旋转等到下面会细说

1.左单旋 

2.右单旋

3.左右双旋

4.右左双旋

这四种旋转就是AVL树最核心的地方了,相比大家也看出来了,在神魔情况下用哪一种旋转,我都用蓝圈圈起来了,这里来总结一下

1.如果 parent.bfchild.bf 都是正数,那么应该进行左单旋(同正左旋)

2.如果 parent.bfchild.bf 都是负数,那么应该进行右单旋(同异右旋)

3.如果 parent.bf 为正 child.bf为负 ,右左双旋

4.如果parent.bf为负  child.bf 为正 ,左右双旋

记的话估计不太好记,但是有一点一定可以记住,同单异双(同号单旋,异号双旋),大家还是重在理解


给出部分代码

        // 2.调节平衡因子while(parent != null){// 先看child是parent的左还是右,判断bf是++还是--if(child == parent.right){// child是parent的左树 bf++parent.bf++;}else{// child是parent的右树 bf++parent.bf--;}// 根据parent的负载因子判断是否需要继续向上调整 bf[-1,0,1]if(parent.bf == 0){// 当前子树平衡代表上面的也已经平衡break;}else if(parent.bf == 1 || parent.bf == -1){// 上面的树不一定平衡,继续向上调整child = parent;parent = parent.parent;}else {if (parent.bf == 2) {if (child.bf == 1) {// 左单旋rotateLeft(parent);} else if (child.bf == -1) {// 右左双旋rotateRL(parent);}} else {if (child.bf == -1) {// 右单旋rotateRight(parent);} else if (child.bf == 1) {// 左右双旋rotateLR(parent);}}// 上述代码走完平衡!!break;}}

下面就是真正写旋转的代码了,我会带着大家实现一个单旋和一个双旋,剩下的就是照着葫芦画瓢,相比大家应该没什么问题


四 . AVL树的旋转

1.左单旋

我们先定义变量,不需要为了节省空间搞成parent.parent.left.right.left 举个例子,实际上没有这么多嵌套

现在来想一个问题: 如果60往上提,那以40为根节点的子树该放在哪? 想清楚这个问题,那么恭喜你

你完全有能力去手撕后面的代码,即使一些细节你考虑不到! 不要往下看,自己想想吧,下面的图会有助于大家理解的

首先要明白一个问题,60提上去之后成为了这颗子树的根节点,根据二叉搜索树的性质,40应该放在60的左侧,答案依然明了,30的左子树指向60的左子树

到这里已经可以手撕一部分代码了

以为到这里就忘了吗? nonono 在这里发出灵魂三问

1. 30是否还有父节点? 如果有的话不改变指向的话会怎么样?

2.subRL有没有可能为空? 如果为空会不会发生空指针异常?

3.parent有没有可能为root?

都是一些细节问题,直接给出代码,大家自行理解,理解不了评论区留言

private void rotateLeft(TreeNode parent) {// 定义变量TreeNode subR = parent.right;TreeNode subRL = subR.left;// 保存parent的父节点 问题1TreeNode pParent = parent.parent;// 问题2 增加空指针判断if(subRL != null) subRL.parent = parent;parent.right = subRL;subR.left = parent;parent.parent = subR;// 问题3 增加判断if(parent == root){root = subR;root.parent = null;}else{if(pParent.left == parent){pParent.left = subR;}else{pParent.right = subR;}subR.parent = pParent;}// 根据图示修改平衡因子parent.bf = subR.bf = 0;}

右旋

public void rotateRight(TreeNode parent){TreeNode subL = parent.left;TreeNode subLR = subL.right;TreeNode pParent = parent.parent;if(subLR != null) subLR.parent = parent;parent.parent = subL;parent.left = subLR;subL.right = parent;if(parent == root){// 当前的parent是根节点root = subL;root.parent = null;}else{if(pParent.left == parent){pParent.left = subL;}else{pParent.right = subL;}subL.parent = pParent;}parent.bf = subL.bf = 0;}

2.左右双旋

左右双旋无非就是经过两次旋转,只要单旋搞清楚了,双旋完全没有难度,值的一提的是,平衡因子的修改需要根据subLR.bf来进行判断,除此之外,没有其他的细节了

    /** 右左双旋* */private void rotateRL(TreeNode parent) {TreeNode subR = parent.right;TreeNode subRL = subR.left;int bf = subRL.bf;rotateRight(subR);rotateLeft(parent);if(bf == -1){parent.bf = subRL.bf = 0;subR.bf = 1;}else{subRL.bf= subR.bf = 0;parent .bf = -1;}}/** 左右双旋* */private void rotateLR(TreeNode parent) {TreeNode subL = parent.left;TreeNode subLR = subL.right;int bf = subLR.bf;rotateLeft(subL);rotateRight(parent);if(bf == -1){subL.bf = subLR.bf = 0;parent.bf = -1;}else{subL.bf = -1;subLR.bf = parent.bf = 0;}}

最后来验证一下是否成功,主要是根据中序遍历的结果是否有序和左右子树的高度差不超过一来进行验证


五 . AVL树性能分析

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询 时高效的时间复杂度,即 O(log2n)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要 维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要 一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修 改,就不太适合


总结

大家多多理解,我们下一篇博客见

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

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

相关文章

2023年【安全员-C证】考试试卷及安全员-C证试题及解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 安全员-C证考试试卷是安全生产模拟考试一点通生成的&#xff0c;安全员-C证证模拟考试题库是根据安全员-C证最新版教材汇编出安全员-C证仿真模拟考试。2023年【安全员-C证】考试试卷及安全员-C证试题及解析 1、【多选…

鸿蒙(HarmonyOS)应用开发——生命周期、渲染控制、状态管理装饰器

生命周期 任何程序都是有一定的生命周期的。生命周期是记录从产生到销毁的过程&#xff1b;如果熟悉前端vue.js的话&#xff0c;就可以很好的理解生命周期。 自定义组件生命周期 ArkTS中&#xff0c;自定义组件提供了两个生命周期函数&#xff1a;aboutToAppear() 和aboutTo…

【数据集】全网最全的常见已公开医学影像数据集

目录 一&#xff0c;极市医学数据集汇总 1.CT 医学图像 ​编辑 2.恶性与良性皮肤癌 3.白内障数据集 4.胸部 X 光图像&#xff08;肺炎&#xff09; 5.用于图像增强的内窥镜真实合成曝光过度和曝光不足帧 6.医学家 7.乳房组织病理学图像 8.皮肤癌 MNIST&#xff1a;HA…

cephadm部署ceph quincy版本

环境说明 IP主机名角色 存储设备 192.168.2.100 master100 mon,mgr,osd,mds,rgw 大于5G的空设备192.168.2.101node101mon,mgr,osd,mds,rgw大于5G的空设备192.168.2.102node102mon,mgr,osd,mds,rgw大于5G的空设备 关闭防火墙 关闭并且禁用selinux 配置主机名/etc/hosts …

JAVA之异常详解

1. 异常的概念与体系结构 1.1 异常的概念 在Java中&#xff0c;将程序执行过程中发生的不正常行为称为异常 1. 算术异常 public class Test {public static void main(String[] args) {System.out.println(10/0);} } 因为 0 不能当被除数&#xff0c;所以报出了异常&#…

JSP宾馆预定管理系统数据库设计过程ER图

**Hi**&#xff0c;今天给大家带来一款使用JSP和Servlet开发的宾馆预定管理系统的论文写作指导。需要使用本项目写文档的童鞋可以好好看看文末附项目的效果查看地址哦~ 一、项目功能 具体的功能看下面这张表&#xff0c;表里面只是截取了主要功能来说的。 员工角色管理员角色员…

JS事件代理(事件委托)

JS事件代理&#xff08;事件委托&#xff09; 前言什么是事件代理事件代理的优点 事件代理实例代码实例&#xff1a; 总结 前言 本文详细讲解JavaScript中关于事件代理技术相关的内容以及源码实例的讲解。那么好&#xff0c;本文正式开始 什么是事件代理 事件代理作为JavaSc…

Nacos身份绕过漏洞复现(QVD-2023-6271)

Nacos身份绕过漏洞复现&#xff08;QVD-2023-6271&#xff09; 环境配置 该漏洞主要用了win10_JAVA的环境&#xff0c;参考网上已有的复现文章&#xff0c;使用jdk-11.0.2_windows-x64_bin.exe 由于2.2.0之后的nacos已将本漏洞修复&#xff0c;所以本次复现使用2.2.0的包 下…

力软vue前端开发:使用params跳转传参404问题解决

问题描述 this.$router.push({ name: page, query: { id: 001 } }) // 根据路由名称 query 的方式跳转传参 使用query传参时&#xff0c;参数会拼接在链接后&#xff0c;点击搜索条件链接参数也还在。用户需要重新进入搜索页面。 所以&#xff0c;使用nameparams进行传参。参…

【JavaEE】多线程 (1)

目录 1. 认识线程&#xff08;Thread&#xff09; 1) 线程是什么 2) 为啥要有线程 3) 进程和线程的区别 2.第⼀个多线程程序 3.多线程的其他创建方式 方法二:实现 Runnable 接⼝ 方法三:匿名内部类 方法四:实现Runable, 重写run, 匿名内部类 方法五:使用lambda表达式…

带你用uniapp从零开发一个仿小米商场_2.创建空白项目及公共样式引入

创建空白项目 打开uniapp 点击新建->项目 如下, 是编辑你项目的名字的地方是你项目存放地址,可以点击浏览器去文件管理里面选地址是模板选择,这里选择默认模板就好是一些其他选择比如uvue能让你项目在编译成软件时运行更快,unicloud能让你用js写后端,且直接就是云开发,g…

Selenium+Pytest自动化测试框架实战

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

掌握3个Mock工具,轻松玩转单元测试

公司要求提升单元测试的质量&#xff0c;提高代码的分支覆盖率和行覆盖率&#xff0c;安排我研究单元测试&#xff0c;指定方案分享并在开发部普及开。 单元测试中的Mock的目的 Mock的主要目的是让单元测试Write Once, Run Everywhere. 即编写一次后&#xff0c;可以在任意时…

第十九章 解读利用pytorch可视化特征图以及卷积核参数(工具)

介绍一种可视化feaature maps以及kernel weights的方法 推荐可视化工具TensorBoard&#xff1a;可以查看整个计算图的数据流向&#xff0c;保存再训练过程中的损失信息&#xff0c;准确率信息等 学习视频&#xff1a; 使用pytorch查看中间层特征矩阵以及卷积核参数_哔哩哔哩…

【机器学习】算法性能评估常用指标总结

考虑一个二分问题&#xff0c;即将实例分成正类&#xff08;positive&#xff09;或负类&#xff08;negative&#xff09;。对一个二分问题来说&#xff0c;会出现四种情况。如果一个实例是正类并且也被 预测成正类&#xff0c;即为真正类&#xff08;True positive&#xff0…

文献速递:人工智能在新生儿重症监护室:现在是时候了

人工智能在新生儿重症监护室&#xff1a;现在是时候了 01 文献速递介绍 文章介绍了AI的多学科特性&#xff0c;包括计算机科学、数学、神经科学和哲学。AI的目标是通过各种计算和算法技术创建智能机器。尽管早期对人类水平AI的预测未能实现&#xff0c;但对AI的期待仍然强烈…

ubuntu20.04配置OpenCV的C++环境

ubuntu20.04配置OpenCV的C环境 这里以opencv-3.4.16为例 复现https://github.com/raulmur/ORB_SLAM2此项目&#xff0c;需安装opencv及其他依赖&#xff0c;可见README.md详情 1.下载opencv源代码 https://opencv.org/releases/ 2.下载OpenCV的扩展包opencv_contrib&#x…

python安装redis库

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

杂货铺 | Windows系统上解压缩tgz文件

文章目录 &#x1f4da;快速终端打开实现 & 解压缩实现步骤&#x1f4da;环境变量的一般配置步骤 & 问题解决思路 &#x1f4da;快速终端打开实现 & 解压缩实现步骤 将对应的tgz文件放入对应的文件夹。快速在指定文件夹下打开终端 打开对应的路径 双击地址栏 然后…

计算机视觉面试题-01

计算机视觉面试通常涉及广泛的主题&#xff0c;包括图像处理、深度学习、目标检测、特征提取、图像分类等。以下是一些可能在计算机视觉面试中遇到的常见问题&#xff1a; 图像处理和计算机视觉基础 图像是如何表示的&#xff1f; 图像在计算机中可以通过不同的表示方法&…