【数据结构】万字长文图解+代码实现AVL树

目录

一、概念

二、图解

1.图解插入

2.图解右单旋

 3.图解左单旋

4.图解右左双旋

 5.图解左右双旋

6.验证是否是AVL树

 三、代码实现

一、概念

AVL树是一种高度平衡的二叉搜索树,得名于其发明者的名字(G. M. Adelson-Velskii和E. M. Landis)。

以下是关于AVL树的一些关键概念:

  1. 定义:AVL树是一种特殊的二叉搜索树,其中任何节点的左右子树的高度差绝对值不会超过1。如果任何时候节点的左右子树高度差超过1,就会通过一系列的旋转操作来恢复平衡。
  2. 特点
    • AVL树的左右子树的高度差的绝对值不超过1。
    • 每个节点的左子树和右子树也都是AVL树。
    • 每个节点都有一个平衡因子,它是左子树的高度减去右子树的高度。
  1. 平衡操作:为了保持树的平衡,AVL树在节点插入或删除后可能会变得不平衡。这时,它会通过旋转来重新平衡,旋转包括单旋转和双旋转两种基本类型。这些旋转确保了树在任何时候都满足平衡条件。
  2. 性能优势:由于AVL树始终保持平衡,因此它的搜索效率较高。在平均和最坏的情况下,搜索、插入和删除操作的时间复杂度都是O(log n),其中n是树中节点的数量。这使得AVL树非常适合用于需要频繁查找和更新操作的场景。

综上所述,AVL树以其严格的平衡条件和高效的旋转操作,保证了在各种操作下都能维持较高的性能,因此在需要快速查找和更新的数据结构应用中非常受欢迎。二叉搜索树在极端情况下数据有序查找速度会变成O(n),为了解决这一问题就有了AVL树,AVL树是一颗高度平衡的二叉搜索树,每棵树的左子树与右子树高度差不会超过1,这样降低了树的高度提升查找效率。适合静态数据的查找,不适合频繁的插入和删除

二、图解
1.图解插入

图解插入

首先我们有如下这样一颗AVL树,我们要在这颗树中插入1,80,90这三个元素,与二叉搜索树插入相同,我们首先需要将该节点进行插入。

在插入之前首先我们如何在代码中判断该节点是否平衡呢,我们需要在每个节点中维持一个平衡因子字段

平衡因子等于右子树子树高度减去左子树高度,由于AVL树左右子树的高度差不会超过2,所以当平衡因子等于2或者-2时这颗树就不平衡了。如上述图中各个节点的平衡因子如下

向上述AVL树中插入1时,首先我们需要按照二叉搜索树插入时一样进行插入

【数据结构】图解二叉搜索树的新增、搜索、删除-CSDN博客

在插入后这个时候我们通过观察发现此时这颗AVL树不平衡了,那么我们先要对这颗树上的节点进行平衡因子的更新,我们从插入的位置开始向上进行遍历,如果当前节点是父亲节点的左孩子就让父亲节点平衡因子--,因为是在左子树进行了插入而右子树不变 bf = 右子树高度- 左子树高度,左子树+1相当于 bf = 右子树高度- (左子树高度 + 1)= bf--,同理如果当前节点是父亲节点的右子树那么就让父亲节点的bf++

这个时候我们在更新平衡因子的过程中发现,有节点平衡因子为-2或者2不平衡了,这个时候我们就需要根据具体情况进行旋转来维持平衡。AVL树的四种旋转策略包括左单旋转、右单旋转、左双旋转和右双旋转

下面将通过图解来展示四种旋转方式

2.图解右单旋

上面在插入1,调整平衡因子的过程中我们发现此处平衡因子为-2,所以我们需要对该节点进行旋转调整,当前平衡因子为-2说明此时左树比较高,我们可以通过右旋让左树的高度降低

如上图所示,此时cur节点中也是左子树比右子树高所以我们只需进行右单旋

右单旋操作如下:我们要将父亲节点的左孩子指向左孩子的右孩子,然后将左孩子指向父亲。同时需要注意修改每个节点的父亲节点

旋转后如图所示

*******subLR此处指向的是44******

1.我们在这里旋转的父亲节点就是根节点,所以我们要更新根节点的指向,但是如果我们更新的是子树呢,我们需要记录这个旋转子树的父亲节点,再完成更新后需要将这颗子树新的根节点赋值给原来父亲节点的父亲节点的左或右

2.如果我们进行旋转的节点它是它父亲节点的右孩子,那么我们再旋转完成后需要将旋转节点的父亲节点的右孩子指向旋转后的树

3.如果我们进行旋转的节点它是它父亲节点的左孩子,那么我们再旋转完成后需要将旋转节点的父亲节点的左孩子指向旋转后的树

在完成上述操作后我们观察此图发现,我们还需要修改旋转后的平衡因子

这个时候我们就完成了右旋,但是需要注意的是,subLR有可能是为空的,在上述代码中subLR为空唯一影响的就是修改subRL的父亲节点,所以在此处我们需要加判断处理

 3.图解左单旋

我们完成了上述插入1的操作,并且通过右单旋维护了AVL树的平衡,此时我们再向上面树中插入80和90

首先我们按照二叉搜索树的插入方式进行插入

然后此时我们需要进行平衡因子的更新

更新过程中一直处于平衡状态,所以不需要进行旋转调整,接着我们插入90

开始调整平衡因子

父亲节点的右子树高,孩子节点中右子树也高所以我们只需进行左单旋转即可

左单旋操作如下:parent节点的left指向右孩子的左孩子也就是subRL,parent右孩子的左指针指向parent,同时parent的父亲指针指向subR,subRL的指针也指向parent,前提是subRL不为空

旋转后如图所示

此时在旋转完这个子树后我们需要将旋转节点的父亲节点指向旋转后新的根节点

与上面右旋转类似需要判断是父亲节点的左节点还是右节点或者是根节点

最后修改subR与parent的平衡因子即可

4.图解右左双旋

在下面这颗AVL树的基础上我们插入27

依旧按照二叉搜索树的规则插入

更新平衡因子

遇到2说明此时这颗parent指向的子树是不平衡的,那么我们观察发现parent的平衡因子为2说明右树高,但是cur指向的parent的右孩子平衡因子是-1说明左树高,这个时候我们需要先对cur进行一次右旋然后再对parent进行左选,也就是说我们需要进行右左双旋。先对cur进行右旋

然后对parent进行左旋转

接下来我们只需调整平衡因子即可,平衡因子的调节需要根据subRL的因子来进行判断所修改哪些节点的平衡因子

此时subRL的平衡因子是-1,我们要进行如下更新

下图中subRL平衡因子为1,进行如下更新

 5.图解左右双旋

在下面这颗AVL树中插入16

按照二叉搜索树的规则进行插入

开始调整平衡因子

先将cur节点进行左旋,然后再将parent进行右旋

在将parent进行右旋

最后我们需要根据subLR的平衡因子进行平衡因子调整,此时subLR是-1

下图中subLR的平衡因子为1

6.验证是否是AVL树

1.采用层序遍历

public void level(TreeNode root) {List<List<TreeNode>> ans = new ArrayList<>();Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while (!queue.isEmpty()) {int size = queue.size();List<TreeNode> row = new ArrayList<>();while (size-- != 0) {TreeNode top = queue.poll();row.add(top);if (top != null) queue.offer(top.left);if (top != null) queue.offer(top.right);}ans.add(row);}for (List<TreeNode> x : ans) {for (TreeNode val : x) {if (val != null)System.out.print(val.val + " ");elseSystem.out.print("null ");}System.out.println();}}

2.判断是否是平衡树

 private int maxDepth(TreeNode root) {// 1. 如果根节点为空则高度为0if (root == null) {return 0;}// 2. 计算左右子树高度int left = maxDepth(root.left);int right = maxDepth(root.right);// 3. 判断是否满足平衡二叉树if (left != -1 && right != -1 &&Math.abs(left - right) <= 1) {return Math.max(left,right) + 1;} else {// 3.1 不满足则返回-1return -1;}}public boolean isBalanced(TreeNode root) {// 1. 如果根节点为空则返回真if (root == null) {return true;}// 2. 根据求高度时返回值判断return maxDepth(root) >= 0;}

 三、代码实现
package avl;import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;public class Test {static class TreeNode {public int val;public int bf;public TreeNode left;public TreeNode right;public TreeNode parent;public TreeNode(int val) {this.val = val;}}public TreeNode root;public void insert(int val) {// 1. 先进行插入// 1.1 如果根节点为空直接创建一个节点让root指向它if (root == null) {root = new TreeNode(val);return;}// 1.2 此时根节点不为空,遍历AVL树找到合适的插入位置TreeNode cur = root, parent = null;while (cur != null) {// 1.2.1 如果当前节点值比待插入的值小那么就去右子树中寻找合适的插入位置if (cur.val < val) {// 1.2.2 记录父亲节点parent = cur;cur = cur.right;} else if (cur.val > val) {// 1.2.3 如果当前节点值比待插入的值大那么就去左子树中寻找合适的插入位置parent = cur;cur = cur.left;} else {// 1.2.4 如果有相同的元素则直接返回return;}}// 1.3 找到合适的位置TreeNode node = new TreeNode(val);if (parent.val > val) {// 1.3.1 如果待插入值比父亲节点小就插入到左边parent.left = node;} else {// 1.3.2 如果待插入值比父亲节点大就插入到右边parent.right = node;}// 1.4 将插入节点的父亲节点进行赋值node.parent = parent;// 2. 进行平衡因子的修改cur = node;while (parent != null) {// 2.1 判断父亲节点的平衡因子变化情况if (cur == parent.left) {parent.bf--;} else {parent.bf++;}// 2.2 对当前父亲节点的平衡因子进行判断if (parent.bf == 0) {// 2.2.1 此时说明已经平衡不再需要调整break;} else if (parent.bf == -1 || parent.bf == 1) {// 2.2.2 此时说明当前父亲节点所在的子树平衡但是父亲节点的父亲平衡是未知的,继续遍历cur = parent;parent = cur.parent;} else {// 2.2.3 此时说明当前树已经不平衡,我们需要通过旋转来调整// 3. 进行旋转if (parent.bf == -2) {// 3.1 左树高if (cur.bf == -1) {// 3.1.1 进行右旋:    parent.bf = -2   cur.bf = -1rotateRight(parent);} else {// 3.1.2 进行左右双旋: parent.bf = -2   cur.bf = 1rotateLeftRight(parent);}} else {// 3.2 右树高if (cur.bf == 1) {// 3.2.1 进行左旋:    parent.bf = 2   cur.bf = 1rotateLeft(parent);} else {// 3.2.2 进行右左双旋: parent.bf = 2   cur.bf = -1rotateRightLeft(parent);}}break;}}}private void rotateLeftRight(TreeNode parent) {// 1. 记录待右旋后左旋的节点TreeNode subL = parent.left;TreeNode subLR = subL.right;int bf = subLR.bf;// 2. 双旋rotateLeft(parent.left);rotateRight(parent);// 3. 更新平衡因子if (bf == -1) {parent.bf = 1;subL.bf = subLR.bf = 0;} else if (bf == 1) {subLR.bf = parent.bf = 0;subL.bf = -1;}}private void rotateRightLeft(TreeNode parent) {// 1. 记录待右旋后左旋的节点TreeNode subR = parent.right;TreeNode subRL = subR.left;int bf = subRL.bf;// 2. 双旋rotateRight(parent.right);rotateLeft(parent);// 3. 更新平衡因子if (bf == -1) {subR.bf = parent.bf = 0;subRL.bf = 1;} else if (bf == 1) {parent.bf = -1;subR.bf = subRL.bf = 0;}}private void rotateLeft(TreeNode parent) {// 1. 记录需要节点的位置TreeNode subR = parent.right;TreeNode subRL = subR.left;// 2. 开始旋转parent.right = subRL;subR.left = parent;if (subRL != null) subRL.parent = parent;TreeNode pParent = parent.parent;parent.parent = subR;// 3. 将原来节点旋转后的子树与其父亲建立联系if (parent == root) {root = subR;subR.parent = null;} else if (parent == pParent.right) {pParent.right = subR;subR.parent = pParent;} else {pParent.left = subR;subR.parent = pParent;}// 4. 调整平衡因子subR.bf = 0;parent.bf = 0;}private void rotateRight(TreeNode parent) {// 1. 记录需要节点的位置TreeNode subL = parent.left;TreeNode subLR = subL.right;// 2. 开始旋转parent.left = subLR;subL.right = parent;if (subLR != null)  subLR.parent = parent;TreeNode pParent = parent.parent;  // 在修改parent的parent之前先记录下来后面会使用parent.parent = subL;// 3. 将原来节点旋转后的子树与其父亲建立联系if (parent == root) {root = subL;subL.parent = null;} else if (parent == pParent.left) {pParent.left = subL;subL.parent = pParent;} else {pParent.right = subL;subL.parent = pParent;}// 4. 调整平衡因子subL.bf = 0;parent.bf = 0;}public static void main(String[] args) {Test tree = new Test();tree.insert(1);tree.insert(11);tree.insert(33);tree.insert(80);tree.insert(55);tree.level(tree.root);System.out.println(tree.isBalanced(tree.root));}/*** 下面代码为验证是否是AVL树* @param root*/public void level(TreeNode root) {List<List<TreeNode>> ans = new ArrayList<>();Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while (!queue.isEmpty()) {int size = queue.size();List<TreeNode> row = new ArrayList<>();while (size-- != 0) {TreeNode top = queue.poll();row.add(top);if (top != null) queue.offer(top.left);if (top != null) queue.offer(top.right);}ans.add(row);}for (List<TreeNode> x : ans) {for (TreeNode val : x) {if (val != null)System.out.print(val.val + " ");elseSystem.out.print("null ");}System.out.println();}}private int maxDepth(TreeNode root) {// 1. 如果根节点为空则高度为0if (root == null) {return 0;}// 2. 计算左右子树高度int left = maxDepth(root.left);int right = maxDepth(root.right);// 3. 判断是否满足平衡二叉树if (left != -1 && right != -1 &&Math.abs(left - right) <= 1) {return Math.max(left,right) + 1;} else {// 3.1 不满足则返回-1return -1;}}public boolean isBalanced(TreeNode root) {// 1. 如果根节点为空则返回真if (root == null) {return true;}// 2. 根据求高度时返回值判断return maxDepth(root) >= 0;}
}

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

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

相关文章

Joe主题网站

一款博客网站源码 发现源码为大家内置了主题 清爽又强大真正的永久可用的一条源码&#xff0c;该版本为整合版本&#xff0c;内置了Joe主题&#xff0c;搭建后直接启用即可~ 安装环境要求&#xff1a; PHP 7.2 以上 MySQL, PostgreSQL, SQLite 任意一种数据库支持&#xff0c;…

【Web】浅聊Java反序列化之Rome——关于其他利用链

目录 前言 JdbcRowSetImpl利用链 BasicDataSource利用链 Hashtable利用链 BadAttributeValueExpException利用链 HotSwappableTargetSource利用链 前文&#xff1a;【Web】浅聊Java反序列化之Rome——EqualsBean&ObjectBean-CSDN博客 前言 Rome中ToStringBean的利用…

CorelDRAW Graphics Suite2024专业图形设计软件Windows/Mac最新25.0.0.230版

CorelDRAW Graphics Suite 2024是一款专业的图形设计软件&#xff0c;它集成了CorelDRAW Standard 2024和其他高级图形处理工具&#xff0c;为用户提供了全面的图形设计和编辑解决方案。 该软件拥有强大的矢量编辑功能&#xff0c;用户可以轻松创建和编辑矢量图形&#xff0c;…

在WSL2中安装多个Ubuntu教程

文章目录 前言一、前期准备1、WSL安装2、Docker安装 二、安装第二个Ubuntu系统1.切换为WSL22.获取Ubuntu16.04的tar文件从容器中导出tar 3. 将tar文件导入WSL4. 设置默认用户 总结 前言 适用于 Linux 的 Windows 子系统 (WSL) 是 Windows 的一项功能&#xff0c;可用于在 Wind…

7-15 2.2 译密码

输入5个字母&#xff0c;将其译成密码&#xff0c;密码规律是&#xff1a;用原来的字母后面第4个字母代替原来的字母。例如&#xff1a;A被E代替&#xff0c;C被G代替。 输入格式: 输入一行5个字符 输出格式: 输出一行5个字符 输入样例: China输出样例: Glmre #include …

C语言--函数指针变量和函数指针数组的区别(详解)

函数指针变量 函数指针变量的作用 函数指针变量是指向函数的指针&#xff0c;它可以用来存储函数的地址&#xff0c;并且可以通过该指针调用相应的函数。函数指针变量的作用主要有以下几个方面&#xff1a; 回调函数&#xff1a;函数指针变量可以作为参数传递给其他函数&…

STL之deque容器代码详解

1 基础概念 功能&#xff1a; 双端数组&#xff0c;可以对头端进行插入删除操作。 deque与vector区别&#xff1a; vector对于头部的插入删除效率低&#xff0c;数据量越大&#xff0c;效率越低。 deque相对而言&#xff0c;对头部的插入删除速度回比vector快。 vector访问…

【Node.js】-闲聊:前端框架发展史

前端框架的发展史是一个不断演进和创新的过程&#xff0c;旨在提高开发效率、优化用户体验&#xff0c;并推动前端技术的不断发展。以下是前端框架发展的主要阶段和关键里程碑&#xff1a; 早期阶段&#xff1a; 在这个阶段&#xff0c;前端主要由HTML、CSS和JavaScript等基础技…

解决ChatGPT发送消息没有反应

ChatGPT发消息没反应 今天照常使用ChatGPT来帮忙码代码&#xff0c;结果发现发出去的消息完全没有反应&#xff0c;即不给我处理&#xff0c;也没有抱任何的错误&#xff0c;按浏览器刷新&#xff0c;看起来很正常&#xff0c;可以查看历史对话&#xff0c;但是再次尝试还是一…

计算机视觉——P2PNet基于点估计的人群计数原理与C++模型推理

简介 人群计数是计算机视觉领域的一个核心任务&#xff0c;旨在估算静止图像或视频帧中的行人数量。在过去几十年中&#xff0c;研究人员在这个领域投入了大量的精力&#xff0c;并在提高现有主流基准数据集性能方面取得了显著进展。然而&#xff0c;训练卷积神经网络需要大规…

【Java.mysql】——增删查改(CRUD)之 增查(CR) 附加数据库基础知识

目录 &#x1f6a9;数据库操作 &#x1f388;创建数据库 &#x1f388;使用数据库 &#x1f388;删除数据库 &#x1f6a9;数据类型 &#x1f6a9;表的操作 &#x1f388;创建表 &#x1f308;查看表结构 &#x1f388;删除表 ❗练习(综合运用) &#x1f5a5;️新增…

腾讯面经学习笔记

&#x1f496; 前言 &#x1f469;‍&#x1f3eb; 参考地址 &#x1f496; 操作系统 1. 进程和线程的区别 本质区别 进程是操作系统资源分配的基本单位线程是任务调度和执行的基本单位 开销方面 每个进程都有独立的代码和数据空间&#xff08;程序上下文&#xff09;&#…