AVL树详解

目录

AVL树的概念

旋转的介绍

单旋转

双旋转

旋转演示

具体实现

通过高度判断的实现

通过平衡因子判断的实现


AVL树的概念

AVL树是一种自平衡的平衡二叉查找树,它是一种高效的数据结构,可以在插入和删除节点时保持树的平衡,从而保证树的操作时间复杂度能够达到O(log n)。

所谓平衡就是保证每个节点的左右子树的高度差不能超过1。例如

bd8189489ce848318a977ea8ebd479f6.jpeg

对AVL树进行插入或删除操作时,可能会导致某些节点的高度差超过1,即不再平衡。这时就需要进行旋转操作来恢复AVL树的平衡性。所以,AVL树的核心内容就是旋转。

旋转的介绍

一般来讲,我们将旋转类型分为两大类。左-左、右-右类型的为单旋转,左-右、右-左类型的为双旋转。下面是这四种旋转的操作方式。

单旋转

AVL树的单旋转指的是在树的某个节点上进行的一种旋转操作,通过左旋或右旋使该节点成为旋转后的子树的根节点,并使树保持平衡状态。在单旋转过程中,节点的左右子树高度变化不超过1,旋转操作其实是把子树的位置进行调整,使得整棵树的平衡因子尽可能地符合平衡树的要求。

具体来说,如果在某个节点的左子树中插入了一个新节点导致该节点的左子树高度比右子树高度多2,那么就需要在该节点进行一次右旋转操作。右旋转将该节点的左子节点变为子树的根节点,该节点的原父节点成为子树的新根节点的右子节点,子树的其他节点位置不变。

同理,如果在某个节点的右子树中插入了一个新节点导致该节点的右子树高度比左子树高度多2,那么就需要在该节点进行一次左旋转操作。左旋转将该节点的右子节点变为子树的根节点,该节点的原父节点成为子树的新根节点的左子节点,子树的其他节点位置不变。

 具体的图解如下: 

5cb4b4d6000447a7b69892d8ff0b10da.png

图片出处:AVL树的旋转图解和简单实现_avl树旋转-CSDN博客

 单旋转的过程可以概况为如下的三个步骤(以下图为模型,以单左旋为例):

        1、让k2原本指向k1的指针现在指向k1内侧的节点

        2、让k1原本指向内侧的指针现在指向k2

        3、让原来指向k2的指针现在改为指向k1,并更新各节点的高度

74a2c69e473f46ec9a0b9d2803ffca99.png

双旋转

AVL树的双旋转是指在某个节点的子树中进行两次旋转操作以保持平衡的一种树旋转方式。双旋转包含两种情况:左旋-右旋和右旋-左旋。

具体来说,假设在AVL树的某个节点的左子树中插入了一个新节点,导致该节点的左子树高度比右子树高度多2,但是进行一次右旋转不能转换成平衡状态,此时需要进行左旋-右旋操作。该操作可以分解为两步:

  1. 对该节点的左子节点进行一次左旋转。

  2. 对该节点进行一次右旋转。

左旋转操作会使得该节点的左子节点变为子树的根节点,同时该节点成为新根节点的右子节点,然后对该节点进行右旋转时,子树的根节点发生了变化,新的根节点是之前的左子节点,原本的根节点成为新节点的右子节点,最后使得整棵树重新平衡。

同样的,假如在AVL树的某个节点的右子树中插入了一个新节点,导致该结点的右子树高度比左子树高度多2,但进行一次左旋转不能转换成平衡状态,此时需要进行右旋-左旋操作。该操作可以分成两步:

  1. 对该节点的右子节点进行一次右旋转。

  2. 对该节点进行一次左旋转。

右旋转操作会使得该节点的右子节点变为子树的根节点,同时该节点成为新根节点的左子节点,然后对该节点进行左旋转时,子树的根节点发生了变化,新的根节点是之前的右子节点,原本的根节点成为新节点的左子节点,最后使得整棵树重新平衡。

具体图解如下:

a3965d75ae3d472785c49be89acffefd.png

图片出处:AVL树的旋转图解和简单实现_avl树旋转-CSDN博客


双旋转其实就是两次单旋转,先将内侧的节点通过单旋转“移出来”到外侧。然后再用一次单旋转,最终成为我们想要的平衡状态。

旋转演示

AVL树动画演示

也可以自己尝试:

AVL Tree Visualzation (usfca.edu)icon-default.png?t=N7T8https://www.cs.usfca.edu/~galles/visualization/AVLtree.html

具体实现

通过高度判断的实现

AVL树一个最直接的方式就是每个节点的信息中增加了一个高度信息(Height),每个节点在插入后向上回溯到根,进行高度信息的调整更新,并判断是否要进行旋转。这种实现方式比较直观好想,每次判断是否需要进行旋转的时候就直接判断高度差即可。

如下是一种实现方式(C语言实现,以递归的方式进行插入和回溯,每个节点只有左右孩子两个指针,维护平衡条件的信息是节点的高度),仅供参考。

/* 忽略了相关头文件 */typedef char ElementType; //暂定节点内容只有单个字符typedef struct AvlTreeNode
{ElementType Data; int Height;struct AvlTreeNode* Left;struct AvlTreeNode* Right;
}AvlTree; int Height(AvlTree* Node)
{if (Node == NULL)return -1;return Node->Height;
}
AvlTree* SingleLeftRotate(AvlTree* k2) //单左旋,LL旋转(k2的由来详见数据结构与算法分析P94)
{//旋转节点AvlTree* k1 = k2->Left; k2->Left = k1->Right;k1->Right = k2;//更新高度k2->Height = max(Height(k2->Left), Height(k2->Right)) + 1;k1->Height = max(Height(k1->Left), Height(k1->Right)) + 1;//返回return k1;
}
AvlTree* SingleRightRotate(AvlTree* k2) //单右旋,RR旋转(k2的由来详见数据结构与算法分析P94)
{//旋转节点AvlTree* k1 = k2->Right;k2->Right = k1->Left;k1->Left = k2; //更新高度k2->Height = max(Height(k2->Left), Height(k2->Right)) + 1;k1->Height = max(Height(k1->Left), Height(k1->Right)) + 1;//返回return k1;
}
AvlTree* DoubleLRRotate(AvlTree* k3) //双左右旋,LR旋转(k3的由来详见数据结构与算法分析P95)
{/*一次双旋转等于两次单旋转。可以理解为先将需要旋转的移至同一方向(即左左、右右这种),然后再用单旋转的方式处理*/k3->Left = SingleRightRotate(k3->Left); return SingleLeftRotate(k3);  
}
AvlTree* DoubleRLRotate(AvlTree* k3) //双右左旋,RL旋转(k3的由来详见数据结构与算法分析P95)
{/*一次双旋转等于两次单旋转。可以理解为先将需要旋转的移至同一方向(即左左、右右这种),然后再用单旋转的方式处理*/k3->Right = SingleLeftRotate(k3->Left);return SingleRightRotate(k3);
}
AvlTree* InsertElement(AvlTree** root, ElementType data)
{//走到空节点(即插入位置),执行插入操作if ((*root) == NULL){//开辟空间并赋值*root = (AvlTree*)calloc(1, sizeof(AvlTree));//成功开辟空间if ((*root) != NULL)(*root)->Data = data; //开辟空间失败else	puts("heap area is full!");} //根节点无内容(值为0),说明为空树,则直接将data插入到根节点else if ((*root)->Data == 0) {(*root)->Data = data;}//data比节点内容小,在左侧插入else if (data < (*root)->Data) {//向左走,并更新左子树内容(*root)->Left = InsertElement(&(*root)->Left, data);	//判断是否需要旋转if (Height((*root)->Left) - Height((*root)->Right) == 2){//如果data小于左子树的data,说明是data插入到左子树的左节点,符合单旋转的情况(3个节点都在左侧)if (data < (*root)->Left->Data) *root = SingleLeftRotate(*root); //左侧单旋转,并更新节点内容//如果data不小于左子树的data,说明是插入到左子树的右节点,是LR型的双旋转情况else*root = DoubleLRRotate(*root); //左右双旋转,并更新节点内容}}//data比节点内容大,在右侧插入else if (data > (*root)->Data) {//向右走,并更新左子树内容(*root)->Right = InsertElement(&(*root)->Right, data); //判断是否旋转if (Height((*root)->Right) - Height((*root)->Left) == 2) {//如果data大于右子树的data,说明是data插入到右子树的右节点,符合单旋转的情况(3个节点都在右侧)if (data > (*root)->Right->Data) *root = SingleRightRotate(*root);  //右侧单旋转,并更新节点内容//如果data不大于右子树的data,说明是插入到右子树的左节点,是RL型的双旋转情况else*root = DoubleRLRotate(*root);}}//data与节点内容值相同else {  /*暂定如果插入的元素内容相同,则什么都不做*/  }//最后更新节点高度(*root)->Height = max(Height((*root)->Left), Height((*root)->Right)) + 1; //返回return *root;
}

通过平衡因子判断的实现

相比通过高度的直观,平衡因子就有些复杂了。这里的平衡因子指的是右子树的高度减左子树的高度,每个节点都有一个平衡因子信息,初始化为0。每次插入节点后也是向上回溯,进行平衡因子的更新调整与判断是否需要旋转,不过这种方式不一定要走到根,如果更新后的平衡因子信息为0,就说明不用再往上调了。

如下是一种实现方式(C++实现,通过迭代循环的方式进行插入和回溯,为了能够不通过递归进行回溯操作,除了基准的左右儿子指针,这种实现方式为每一个节点还为维护了一个双亲指针,维护平衡条件的信息是平衡因子),仅供参考。

/* 忽略了相关头文件 */// 节点类型
template<typename T>
struct AVLNode
{AVLNode(const T& val): _val(val), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){}T _val;  // 数据内容int _bf; // 平衡因子:右高度减左高度AVLNode<T>* _left;AVLNode<T>* _right;AVLNode<T>* _parent;
};// 树的类型
template<typename T>
class AVLtree
{AVLNode<T>* _root = nullptr; // 根节点
public:bool insert(const T& val){/* 初始情况,树为空 */if (_root == nullptr){_root = new AVLNode<T>(val);return true;}/* 一般情况,插入数据 */AVLNode<T>* pre = nullptr;AVLNode<T>* cur = _root;// 先找到对应位置while (cur){// 要插入数据已存在 - 插入失败if (val == cur->_val)return false;// 要插入数据不存在,继续寻找pre = cur;if (val > cur->_val)cur = cur->_right;else if (val < cur->_val)cur = cur->_left;}// cur找到了插入位置,new一个新节点并插入cur = new AVLNode<T>(val);if (val > pre->_val)pre->_right = cur;elsepre->_left = cur;cur->_parent = pre;/* 向上回溯,随之更新平衡因子,并进行旋转调整 */while (pre != nullptr){// 根据cur的位置,更新父节点的平衡因子信息if (cur == pre->_right)pre->_bf++;elsepre->_bf--;// bf为0或者正负2的情况都退出,所以就合并处理了// 因为2这里给限制死了,所以不会出现abs(bf)大于等于3的情况if (abs(pre->_bf) != 1){if (abs(pre->_bf) == 2)rotatenode(pre, cur->_bf);break;}// 没遇到特殊情况,继续向上回溯cur = pre;pre = cur->_parent;}return true;}
private:// 需要旋转节点的情况 - 旋转的4种情况void rotatenode(AVLNode<T>* parent, int cur_bf){if (parent->_bf == 2){if (cur_bf == 1)rotateRR(parent);else if (cur_bf == -1)rotateRL(parent);}else if (parent->_bf == -2){if (cur_bf == 1)rotateLR(parent);else if (cur_bf == -1)rotateLL(parent);}}/* 旋转的4个函数 */// 左左单旋(节点都在左侧的情况)void rotateLL(AVLNode<T>* parent){		// 调整节点位置及父子关系AVLNode<T>* cur = parent->_left;AVLNode<T>* rchild = cur->_right;parent->_left = rchild;if (rchild != nullptr)rchild->_parent = parent;cur->_right = parent;// 进行旋转AVLNode<T>* super = parent->_parent;cur->_parent = super;parent->_parent = cur;AVLNode<T>*& port = super == nullptr ? _root : (super->_left == parent ? super->_left : super->_right);port = cur;// 调节平衡因子cur->_bf = 0;parent->_bf = 0;}// 右右单旋(节点都在右侧的情况)void rotateRR(AVLNode<T>* parent){// 调整节点位置及父子关系AVLNode<T>* cur = parent->_right;AVLNode<T>* lchild = cur->_left;parent->_right = lchild;if (lchild != nullptr)lchild->_parent = parent;cur->_left = parent;// 进行旋转AVLNode<T>* super = parent->_parent;cur->_parent = super;parent->_parent = cur;AVLNode<T>*& port = super == nullptr ? _root : (super->_left == parent ? super->_left : super->_right);port = cur;// 调节平衡因子cur->_bf = 0;parent->_bf = 0;}// 左右双旋void rotateLR(AVLNode<T>* parent){// 先把里面的节点旋出来,再按照单旋转处理AVLNode<T>* cur = parent->_left;AVLNode<T>* sub = cur->_right;int bf = sub->_bf;rotateRR(cur);rotateLL(parent);// 调整平衡因子if (bf == 1)cur->_bf = -1;else if (bf == -1)parent->_bf = 1;}// 右左双旋void rotateRL(AVLNode<T>* parent){// 先把里面的节点旋出来,再按照单旋转处理AVLNode<T>* cur = parent->_right;AVLNode<T>* sub = cur->_left;int bf = sub->_bf;rotateLL(cur);rotateRR(parent);// 调整平衡因子if (bf == 1)parent->_bf = -1;else if (bf == -1)cur->_bf = 1;}
};

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

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

相关文章

在HTML单页面中,使用Bootstrap框架的多选框如何提交数据

1.引入Bootstrap CSS和JavaScript文件&#xff1a;确保在HTML页面的标签内引入Bootstrap的CSS和JavaScript文件。可以使用CDN链接或者下载本地文件。 <link rel"stylesheet" href"https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css&q…

04【保姆级】-GO语言指针

之前我学过C、Java、Python语言时总结的经验&#xff1a; 先建立整体框架&#xff0c;然后再去抠细节。先Know how&#xff0c;然后know why。先做出来&#xff0c;然后再去一点点研究&#xff0c;才会事半功倍。适当的囫囵吞枣。因为死抠某个知识点很浪费时间的。对于GO语言&a…

16.字符连接

#include<stdio.h> #include <cstring> int main(){char s1[44];char s2[33];scanf("%s",s1);scanf("%s",s2);strcat(s1,s2) ;printf("连接两个字符为&#xff1a;%s ",s1); return 0;}

HashMap源码分析(一)

存储结构 说明&#xff1a;本次讲解的HashMap是jdk1.8中的实现&#xff0c;其他版本可能有差异 内部是由Node节点数组组成&#xff0c;Node节点之间又由链表或红黑树组成。 图是网上找的&#xff0c;实在不想画 属性介绍 //存储数据的数组&#xff0c;初次使用时初始化&…

基于CSP的运动想象EEG分类任务实战

基于运动想象的公开数据集&#xff1a;Data set IVa (BCI Competition III)1 数据描述参考前文&#xff1a;https://blog.csdn.net/qq_43811536/article/details/134224005?spm1001.2014.3001.5501 EEG 信号时频空域分析参考前文&#xff1a;https://blog.csdn.net/qq_4381153…

xdcms漏洞合集-漏洞复现

目录 xdcms v3.0.1漏洞 环境搭建 代码审计 目录总览 配置文件总览 登陆处sql注入 漏洞分析 漏洞复现 注册处sql注入漏洞 漏洞分析 漏洞复现 getshell 任意文件删除 xdcms订餐网站管理系统v1.0漏洞 简介 环境搭建 全局变量的覆盖 漏洞分析 漏洞复现 后台任意…

6个机器学习可解释性框架

1、SHAP SHapley Additive explanation (SHAP)是一种解释任何机器学习模型输出的博弈论方法。它利用博弈论中的经典Shapley值及其相关扩展将最优信贷分配与局部解释联系起来. 举例&#xff1a;基于随机森林模型的心脏病患者预测分类 数据集中每个特征对模型预测的贡献由Shap…

PDF Expert for mac(苹果电脑专业pdf编辑器)兼容12系统

PDF Expert是macOS平台上的一款优秀的PDF阅读和编辑工具&#xff0c;由Readdle公司开发。它不仅拥有方便、易用的界面&#xff0c;还具备诸多功能&#xff0c;比如编辑PDF文件、添加批注、填写表格、签署文件、合并文档等。安装:PDF Expert for Mac(PDF编辑阅读转换器)v3.5.2中…

探讨m6调控因子与人类癌症之间的因果关系,纯生信也能轻松上5+

今天给同学们分享一篇生信文章“m6A Regulators Is Differently Expressed and Correlated With Immune Response of Esophageal Cancer”&#xff0c;这篇文章发表在Front Cell Dev Biol期刊上&#xff0c;影响因子为5.5。 结果解读&#xff1a; m6A调控因子在基因组中的异常与…

【 毕设项目源码推荐 javaweb 项目】 基于 springboot+vue 的图书个性化推荐系统的设计与实现(springboot003)

简介 :::warning 【 毕设项目源码推荐 javaweb 项目】 基于 springbootvue 的图书个性化推荐系统的设计与实现适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负…

15.数组逆置【不是为啥我第四行不太对呢?】

#include<stdio.h>int fun(int a[4][4]){int i,j;int b[4][3];} int main(){int i,j;int a[3][4], b[4][3];for (i0;i<3;i)for(j0;j<4;j)scanf("%d",&a[i][j]);for (i0;i<3;i)for(j0;j<4;j)b[j][i]a[i][j];printf("逆置后&#xff1a;\n&…

【Linux】文件重定向以及一切皆文件

文章目录 前言一、重定向二、系统调用dup2三、重定向的使用四、一切皆文件 前言 Linux进程默认情况下会有3个缺省打开的文件描述符&#xff0c;分别是标准输入0&#xff0c; 标准输出1&#xff0c; 标准错误2&#xff0c; 0,1,2对应的物理设备一般是&#xff1a;键盘&#xff…

本地数据库迁移到云端服务器

工具迁移xtrabackup 创建云服务器——通过云服务器提供的公网地址远程连接XShell——利用迁移工具将数据库从本地迁移到云服务器 &#xff08;1&#xff09;创建云服务器 &#xff08;2&#xff09;远程连接XShell &#xff08;3&#xff09;yum安装mysql &#xff08;4&…

梳理自动驾驶中的各类坐标系

目录 自动驾驶中的坐标系定义 关于坐标系的定义 几大常用坐标系 世界坐标系 自车坐标系 传感器坐标系 激光雷达坐标系 相机坐标系 如何理解坐标转换 机器人基础中的坐标转换概念 左乘右乘的概念 对左乘右乘的理解 再谈自动驾驶中的坐标转换 本节参考文献 自动驾驶…

数据结构:Map和Set(2):相关OJ题目

目录 136. 只出现一次的数字 - 力扣&#xff08;LeetCode&#xff09; 771. 宝石与石头 - 力扣&#xff08;LeetCode&#xff09; 旧键盘 (20)__牛客网 (nowcoder.com) 138. 随机链表的复制 - 力扣&#xff08;LeetCode&#xff09; 692. 前K个高频单词 - 力扣&#xff08…

【Qt绘制小猪】以建造者模式绘制小猪

效果 学以致用&#xff0c;使用设计模式之建造者模式绘制小猪。 代码 接口&#xff1a;申明绘制的步骤 PigBuilder.h #ifndef PIGBUILDER_H #define PIGBUILDER_H#include <QObject> #include <QPainter>class PigBuilder : public QObject {Q_OBJECT public:ex…

Vue3 学习笔记

vue3 官网&#xff1a;简介 | Vue.js (vuejs.org) 1. 环境搭建与启动 npm create vuelatest 这一指令将会安装并执行 create-vue&#xff0c;它是 Vue 官方的项目脚手架工具 之后&#xff0c;你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示&#xff1a; ✔ …

AI 绘画 | Stable Diffusion 高清修复、细节优化

前言 在 Stable Diffusion 想要生成高清分辨率的图片。在文生图的功能里&#xff0c;需要设置更大的宽度和高度。在图生图的功能里&#xff0c;需要设置更大的重绘尺寸或者重绘尺寸。但是设置完更大的图像分辨率&#xff0c;需要更大显存&#xff0c;1024*1024的至少要电脑的空…

介绍两个好用又好玩的大模型工具

先让数字人跟大家打个招呼吧。 我的AI数字人会手语了 发现没&#xff0c;我的数字人本周又学了一个新技能&#xff1a;手语。 这些数字人都是通过AI生成的。 但数字人不是今天的主题&#xff0c;今天要跟大家聊聊大模型。 自从大模型出现后&#xff0c;很多人&#xff08;包…

模态对话框和非模态对话框

创建到堆区这样非模态对话框就不会一闪而过 .exec使程序进入阻塞状态 ()[]{}lambda表达式 55号属性可以在对话框关闭的时候将堆区的内存释放掉从而防止内存泄露