【数据结构】AVL树相关知识详细梳理

1. AVL树的概念

        AVL的全称是Adelson-Velsky-Landis,其名称来源于其发明者Adelson、Velsky和Landis,

平衡二叉树搜索树

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


        AVL树是具有以下性质的二叉搜索树:

        1. 它的左右子树都是AVL树。
        2. 左右子树高度之差(简称平衡因子)的绝对值不超过1。

 

        AVL树是高度平衡的二叉搜索树如果它有n个结点,其高度可保持在log_2N,搜索的时间复杂度O(log_2N)。并且克服了普通二叉搜索树可能退化,导致搜索效率大大降低的缺点。

2. AVL树原理
        

2.1 节点结构

        AVL树是通过对节点进行调整来控制树的高度以达到两端平衡,那么它是如何调整节点的呢?

        当插入节点打破了AVL树的规则----左右子树高度之差的绝对值超过1后,就会对节点进行旋转来降低子树的高度,来达到左右子树的相对平衡,避免树结构退化。

        为了方便对节点进行调整和检测,我们引入平衡因子的概念,即在每个树节点中增加一个int类型的变量来记录左右子树的高度差(这里是右子树高度 减 左子树高度),这样一来,通过分析平衡因子的大小,我们就可以判断节点是否需要旋转处理。

        显然,平衡因子的更新很多时候是牵一发而动全身的,例如:

        因此,通过平衡因子维护树结构的平衡既带来了便利,又带来了麻烦,我们往往需要从插入节点开始向上不断更新平衡因子,为了解决二叉树在向上遍历时的麻烦,我这里将节点设置为三叉,即一个节点同时拥有 父节点,左子树节点,右子树节点的指针。 

 

2.2 更新平衡因子

        在对节点进行调整前,首先要维护平衡因子,每次插入或旋转节点后,都要对相关平衡因子进行更新,旋转操作也是当发现平衡因子值异常(绝对值大于1)时才执行的。

        首先,二叉搜索树的插入节点一定是叶节点,插入节点为父节点的左子树的时候,父节点的平衡因子 -1,插入节点为父节点的右子树时,父节点的平衡因子 +1。

        然后,为了向上不断更新平衡因子,我们需要总结平衡因子更新的规律:

        1. 更新后的平衡因子为 1 或 -1(说明插入节点前,父节点的平衡因子为0),说明子树的高度变高,需要继续向上更新平衡因子。

        2. 更新后的平衡因子为 0(说明插入节点前,父节点的平衡因子为-1或1,插入节点在较矮的树那边),说明子树的高度不变,不需要再向上更新平衡因子了。

        3. 更新后的平衡因子为2 或 -2(说明插入节点前,父节点的平衡因子为-1或1,且插入节点在较高的子树那边),此时破坏了平衡规则,需要进行旋转调整。

        情况1:

       

        情况2: 

        情况3: 

 

2.3 旋转 (插入)

        二叉平衡搜索树通过旋转操作来改变节点之间的连接关系以降低数的高度,保持平衡,同时不破坏搜索树的规则。那么是如何旋转的呢?

        首先,旋转分为四种:

        1. 右单旋。

        2. 左单旋。

        3. 右左双旋。

        4. 左右双旋。

        下面通过概括图来分别描述这几种旋转对应的情况,以及如何完成旋转:

        右单旋:

        从图中可以看出,当根节点平衡因子为-2(此时左子树必定存在),且其左子树平衡因子为-1时,要进行右单旋,调整完毕后,subL和pRoot的平衡因子皆更新为0,此时子树的高度等于插入前的高度,不需要再向上更新。

        左单旋:

        类似于右单旋,当根节点平衡因子为2(此时右子树必定存在),且其左子树平衡因子为1时,要进行左单旋,调整完毕后,subL和pRoot的平衡因子皆更新为0,此时子树的高度等于插入前的高度,不需要再向上更新。 

        右左双旋:

        左右双旋:

        和右左双旋是镜像的操作,这里不再详细说明。 

        看到这里,想必你以及懂得了什么叫做旋转,也就是把下面的节点“转”到上面来,把上面的“转”下去,以到达平衡左右子树,缩小左右子树高度差的效果。

        2.4 删除

                因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,最差情况下一直要调整到根节点的位置,原理比插入更加繁琐,但也是基于旋转的原理之上的,我们只理解插入时的情况就够用了,感兴趣可以自行了解AVL树删除的实现原理。
 

3. AVL树结构模拟实现

        总体结构:

template<class T>//AVLTree节点
struct AVLTreeNode
{AVLTreeNode(const T& data = T()): _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr), _data(data), _bf(0){}//使用三叉结构方便后续更新平衡因子AVLTreeNode <T>* _pLeft;//左节点指针AVLTreeNode <T>* _pRight;//右节点指针AVLTreeNode <T>* _pParent;//父节点指针T _data;int _bf; //节点的平衡因子
};//AVL: 二叉搜索树 + 平衡因子的限制
template<class T>
class AVLTree
{typedef AVLTreeNode<T> Node;
public:AVLTree(): _pRoot(nullptr){}//在AVL树中插入值为data的节点bool Insert(const T& data);//AVL树的验证bool IsAVLTree(){return _IsAVLTree(_pRoot);}//AVL树的遍历//void Inorder()//{//	return _Inorder(_pRoot);//}private://AVL树的遍历//void _Inorder(Node* pRoot);//根据AVL树的概念验证pRoot是否为有效的AVL树bool _IsAVLTree(Node* pRoot);//获取树高度size_t _Height(Node* pRoot);//右单旋void RotateR(Node* pParent);//左单旋void RotateL(Node* pParent);//右左双旋void RotateRL(Node* pParent);//左右双旋void RotateLR(Node* pParent);Node* _pRoot;//根节点
};

 获取树高:

//获取树高度
template<class T>
size_t AVLTree<T>::_Height(Node* pRoot)
{if (pRoot == nullptr)return 0;size_t leftsize = _Height(pRoot->_pLeft)+1;size_t rightsize = _Height(pRoot->_pRight)+1;return leftsize >= rightsize ? leftsize : rightsize;
}

插入节点:

template<class T>
bool AVLTree<T>::Insert(const T& data)
{//树为空,直接插入if (_pRoot == nullptr){_pRoot = new Node(data);return true;}//根据比较规则找到插入位置Node* pcur = _pRoot;Node* parent = nullptr;while (pcur){if (data == pcur->_data)return false;parent = pcur;if (data > pcur->_data)pcur = pcur->_pRight;elsepcur = pcur->_pLeft;}//直接插入并更新平衡因子if (data > parent->_data){pcur = new Node(data);parent->_pRight = pcur;pcur->_pParent = parent;parent->_bf += 1;}else{pcur = new Node(data);parent->_pLeft = pcur;pcur->_pParent = parent;parent->_bf -= 1;}while (parent){// 如果插入位置子树的根平衡因子为0,则子树高度不变,不需要向上更新if (parent->_bf == 0){//插入完成,返回真return true;}// 违反avl树规则,需要旋转if (parent->_bf == 2){//右子树平衡因子为1,需要左单旋if (parent->_pRight->_bf == 1){RotateL(parent);//旋转后子树高度不变,不需要继续向上更新return true;}//右子树平衡因子为-1,需要右左双旋else if (parent->_pRight->_bf == -1){RotateRL(parent);//双旋后子树高度不变,不需要再向上更新return true;}}// 违反AVL树规则,需要旋转if (parent->_bf == -2){//左子树平衡因子为-1,需要右单旋if (parent->_pLeft->_bf == -1){RotateR(parent);//旋转后子树高度不变,不需要继续向上更新return true;}//左子树平衡因子为1,需要左右双旋else if (parent->_pLeft->_bf == 1){RotateLR(parent);//双旋后子树高度不变,不需要再向上更新return true;}}// 如果插入位置子树的根平衡因子为1/-1,则子树高度增加,需要向上更新if (parent->_bf == 1 || parent->_bf == -1){//如果parent不为根节点if (parent->_pParent){if (parent->_data > parent->_pParent->_data)parent->_pParent->_bf += 1;elseparent->_pParent->_bf -= 1;}}//向上更新parent = parent->_pParent;}
}

        实现插入代码时,重点要理清插入以及旋转的逻辑,利用节点的三叉结构,循环向上更新平衡因子并进行旋转,这也是最难的部分。 

右单旋:

//右单旋
template<class T>
void AVLTree<T>::RotateR(Node* pParent)
{Node* subL = pParent->_pLeft;Node* subLR = subL->_pRight;subL->_pParent = pParent->_pParent;//如果原pParent不为根节点if (pParent->_pParent){if (subL->_data > subL->_pParent->_data)subL->_pParent->_pRight = subL;elsesubL->_pParent->_pLeft = subL;}subL->_pRight = pParent;pParent->_pParent = subL;pParent->_pLeft = subLR;//如果subLR不为空if (subLR)subLR->_pParent = pParent;//更新平衡因子pParent->_bf = 0;subL->_bf = 0;//若subL为根节点,更新根节点if (subL->_pParent == nullptr)_pRoot = subL;
}

左单旋:

//左单旋
template<class T>
void AVLTree<T>::RotateL(Node* pParent)
{Node* subR = pParent->_pRight;Node* subRL = subR->_pLeft;subR->_pParent = pParent->_pParent;//如果原pParent不为根节点if (pParent->_pParent){if (subR->_data > subR->_pParent->_data)subR->_pParent->_pRight = subR;elsesubR->_pParent->_pLeft = subR;}subR->_pLeft = pParent;pParent->_pParent = subR;pParent->_pRight = subRL;//如果subRL不为空if (subRL)subRL->_pParent = pParent;//更新平衡因子pParent->_bf = 0;subR->_bf = 0;//若subL为根节点,更新根节点if (subR->_pParent == nullptr)_pRoot = subR;
}

右左双旋:

//右左双旋
template<class T>
void AVLTree<T>::RotateRL(Node* pParent)
{Node* subR = pParent->_pRight;Node* subRL = subR->_pLeft;//记录subRL原先的平衡因子int subRLbf = subRL->_bf;//先对subR右旋RotateR(subR);//再对pParent左旋RotateL(pParent);//更新平衡因子subRL->_bf = 0;//如果subRL原先的平衡因子为-1if (subRLbf == -1){pParent->_bf = 0;subR->_bf = 1;}//如果subRL原先的平衡因子为1else if (subRLbf == 1){pParent->_bf = -1;subR->_bf = 0;}//如果subRL原先的平衡因子为0else{pParent->_bf = 0;subR->_bf = 0;}//双旋后子树高度不变,不需要再向上更新
}

 左右双旋:

//左右双旋
template<class T>
void AVLTree<T>::RotateLR(Node* pParent)
{Node* subL = pParent->_pLeft;Node* subLR = subL->_pRight;//记录subLR原先的平衡因子int subLRbf = subLR->_bf;//先对subL左旋RotateL(subL);//再对pParent右旋RotateR(pParent);//更新平衡因子subLR->_bf = 0;//如果subRL原先的平衡因子为-1if (subLRbf == -1){pParent->_bf = 1;subL->_bf = 0;}//如果subRL原先的平衡因子为1else if (subLRbf == 1){pParent->_bf = 0;subL->_bf = -1;}//如果subRL原先的平衡因子为0else{pParent->_bf = 0;subL->_bf = 0;}
}

验证用代码: 

 最后可以搭配两个验证AVL树的代码:

//AVL树的遍历(检测结果是否有序)
/*template<class T>
void AVLTree<T>::_Inorder(Node* pRoot)
{if (pRoot == nullptr)return;if (pRoot->_pLeft)_Inorder(pRoot->_pLeft);cout << pRoot->_data << ' ';if (pRoot->_pRight)_Inorder(pRoot->_pRight);
}*///根据AVL树的概念验证pRoot是否为有效的AVL树
template<class T>
bool AVLTree<T>::_IsAVLTree(Node* pRoot)
{//空树为AVL树,返回trueif(pRoot == nullptr)return true;//计算左右子树高度差int diff = _Height(pRoot->_pRight) - _Height(pRoot->_pLeft);if (diff != pRoot->_bf || (diff > 1 || diff < -1))//左右子树高度绝对值大于1,返回falsereturn false;else//继续向下检查左右子树是否是AVL树return true && _IsAVLTree(pRoot->_pRight) && _IsAVLTree(pRoot->_pLeft);
}

还可以依次插入以下节点同时画图验证正确性:

        1. {16, 3, 7, 11, 9, 26, 18, 14, 15}
        2. {4, 2, 6, 1, 3, 5, 15, 7, 16, 14}

4. 总结 

理解:

        总的来说,AVLTree实现的关键在于理解旋转原理,尤其是双旋中的不同情况。

性能:

        AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log_2N

        但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但如果一个结构还需要经常修改,就不太适合。

 

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

常见的TTL,RS232,RS485,IIC,SPI,UART之间的联系和区别

简单总结 图片来源 RS232,RS485可参考&#xff0c;IIC&#xff0c;SPI,UART可参考 烧录程序中常听到的一句话就是USB转TTL&#xff0c;但严格来说算是USB传输数据的协议转换成TTL&#xff08;Transistor-Transistor Logic&#xff09;协议传输数据。首先&#xff0c;usb是常见…

15、网络安全合规由来与要素

数据来源&#xff1a;1.网络安全合规由来与要素_哔哩哔哩_bilibili 合规由来 合规&#xff08;Compliance&#xff09;&#xff1a;指服从、顺从和遵从的行为&#xff0c;强调使公司的经营活动与法律、监管及内部规则保持一致。合规涉及公司适应法律法规及社会规范等规则的经营…

[附源码]在线音乐系统+SpringBoot+Vue前后端分离

今天带来一款优秀的项目&#xff1a;在线音乐系统源码 。 系统采用的流行的前后端分离结构&#xff0c;内含功能包括 "管理后台"&#xff0c;“用户端”&#xff0c;“评论系统”&#xff0c;“歌手&#xff0c;歌曲管理”&#xff0c;“用户系统”,"统计"…

bugku-头等舱

根据题目提示&#xff0c;查看请求头试一下&#xff0c; 得到flag&#xff0c;直接提交

【JavaScript】LeetCode:56-60

文章目录 56 路径总和Ⅲ57 二叉树的最近公共祖先58 二叉树中的最大路径59 岛屿数量60 腐烂的橘子 56 路径总和Ⅲ 递归遍历每个节点所有可能的路径。pathSum()&#xff1a;返回所有节点满足条件的路径数目&#xff0c;traversal()&#xff1a;返回当前遍历节点满足条件的路径数目…

CloudMusic:免费听歌

本文所涉及所有资源均在 传知代码平台可获取。 目录 概述 演示效果 视频演示 图片展示 核心逻辑 获取歌曲图片 提取搜索结果 使用方式 部署方式 Docker部署1 构建镜像 Web站点部署2 附件下载 概述 CloudMusic是一款全网歌曲免费听的web项目&#xff0c;无需任何数据库&#x…

19、网络安全合规复盘

数据来源&#xff1a;5.网络安全合规复盘_哔哩哔哩_bilibili

【Java】异常处理 —— Throwable 及其应用

通过一张图来展示Throwable类的继承体系&#xff0c;如图2所示。 图2 Throwable异常体系结构图 ● Error类称为错误类&#xff0c;它表示Java运行时产生的系统内部错误或资源耗尽的错误&#xff0c;是比较严重的&#xff0c;仅靠修改程序本身是不能恢复执行的&#xff0c;例如…

3D建模软件 | Blender v4.2.2 绿色版

Blender是一款功能强大的免费开源3D创作套件&#xff0c;适用于创建3D可视化效果&#xff0c;如静态图像、3D动画、视觉特效以及视频编辑。Blender以其跨平台兼容性、高效内存管理、统一的工作流程和活跃的社区支持而受到独立艺术家和小型工作室的青睐。 它提供了从建模、渲染…

Ubuntu下安装向日葵:闪退

下载 https://sunlogin.oray.com/download 初次安装 $ sudo dpkg -i SunloginClient_15.2.0.63064_amd64.deb 正在选中未选择的软件包 sunloginclient。 (正在读取数据库 ... 系统当前共安装有 234281 个文件和目录。) 准备解压 SunloginClient_15.2.0.63064_amd64.deb ..…

使用bat命令在没有java的环境下启动jar包

使用bat命令在没有java的环境下启动jar包 先看一下目录下面的文件 里面有三个比较重要的文件 clean.bat&#xff1a;用于清除占用程序的端口 一键启动_x64.bat&#xff1a;用于启动全部的项目 jre8_win64&#xff1a;用于jar所需要的java环境 注意事项&#xff1a; 关于jar…

MySQL 中优化 COUNT()查询的实用指南

在 MySQL 数据库的使用中&#xff0c;我们经常会用到 COUNT()函数来统计行数或满足特定条件的行数。然而&#xff0c;在处理大规模数据时&#xff0c;COUNT()查询可能会变得非常缓慢&#xff0c;影响数据库的性能。那么&#xff0c;如何在 MySQL 中优化 COUNT()查询呢&#xff…

Redis一些简单通用命令和认识常用数据类型和编码方式

通用命令 get() / set() 这是Redis中两个最为核心的命令。 set插入 这里的key 和 value都是字符串&#xff0c;我们可以加双引号 或者单引号&#xff0c;或者不加。 get查找 如果查询的key值不存在&#xff0c;那么会返回一个 nil &#xff0c;也就是代表空 在Redis中命令…

【C++位图】构建灵活的空间效率工具

目录 位图位图的基本概念如何用位图表示数据位图的基本操作setresettest 封装位图的设计 总结 在计算机科学中&#xff0c;位图&#xff08;Bitmap&#xff09;是一种高效的空间管理数据结构&#xff0c;广泛应用于各种场景&#xff0c;如集合操作、图像处理和资源管理。与传统…

什么是开放式耳机?具有什么特色?非常值得入手的蓝牙耳机推荐

开放式耳机是当下较为热门的一种耳机类型。它具有以下特点&#xff1a; 设计结构&#xff1a; 呈现开放式的构造&#xff0c;不会完全堵住耳道。如此一来&#xff0c;外界声音能够较容易地被使用者听到&#xff0c;在使用耳机时可以保持对周围环境的察觉。比如在户外&#xf…

绿色新纪元:光伏技术飞跃与能源体系重塑

近年来&#xff0c;光伏电池技术取得了突破性进展。新型高效光伏材料如钙钛矿、有机光伏等不断涌现&#xff0c;这些材料在转换效率和稳定性上均表现出色&#xff0c;为光伏产业注入了新的活力。同时&#xff0c;光伏组件的智能化、轻量化设计也日益成为趋势&#xff0c;使得光…

Go基础学习06-Golang标准库container/list(双向链表)深入讲解;延迟初始化技术;Element;List;Ring

基础介绍 单向链表中的每个节点包含数据和指向下一个节点的指针。其特点是每个节点只知道下一个节点的位置&#xff0c;使得数据只能单向遍历。 示意图如下&#xff1a; 双向链表中的每个节点都包含指向前一个节点和后一个节点的指针。这使得在双向链表中可以从前向后或从后…

403高效绕过目录扫描工具

403高效绕过目录扫描工具 简介 在安全测试中&#xff0c;安全测试人员信息收集时可使用此工具来进行目录枚举&#xff0c;目录进行指纹识别&#xff0c;枚举出来的403状态目录可尝试进行绕过&#xff0c;绕过403有可能获取管理员权限&#xff0c;不影响dirsearch原本功能使用。…

提升效率,C4D云渲染教程来了

因为C4D主要搭配的渲染器OCtane和Redshift都是GPU渲染器&#xff0c;阿诺德渲染器也可能直接用GPU渲染&#xff0c;所以大部分C4D渲染农场都支持用RTX2080、3090、4090系列显卡云渲染&#xff0c;云渲染追求速度&#xff0c;分机渲染任务&#xff0c;比如分100台机器渲染一个相…

wireshark1

注意看title&#xff0c;管理员的密码即为答案&#xff0c;那么咱们就直接去过找POST请求的数据包就可以了 找到flag&#xff0c;游戏结束~