当前位置: 首页 > news >正文

深入理解C++系列(15)——AVL树


⭐️博主:此生决int-@CSDN博客

速胜派就是最大的投降派!!!

🔥热门专栏🔥

深入理解 C++ 系列算法系列

快速复习系列Java 速通系列


文章目录

    • 上期回顾
  • AVL树
    • AVL树简介
      • 1,AVL树概念:
    • AVL树的实现
      • AVL树的结构
      • insert插入函数的实现⭐️⭐️⭐️⭐️⭐️
      • 平衡因子的维护
        • 平衡因子的几种情况:
      • 插入后,平衡因子为2和-2时
      • 右单旋
      • 左单旋
      • 左右双旋
        • 情况1:插入subLR的左子树
        • 情况2:插入subLR的右子树(与情况一差不多)
        • 情况3,特殊情况,h=0,即subLR就是插入节点
      • 右左双旋(同理,会了左右就会右左)
        • 情况1
        • 情况2
        • 情况3,特殊情况,h=0
    • insert完整实现代码
      • 左单旋代码
      • 右左双旋代码
    • IsBalanceTree判断一颗树是不是AVL树
    • 总结:
    • 下期预告
    • 红黑树
    • 结语

上期回顾

上一篇我们主要学习了如何使用map和set,了解了他们相关的接口,做了相关的一些算法题,那么今天,我们就来看看,怎么保证二叉搜索树的高度不会太高,达到logN的效率的呢?那要我们学完今天的AVL树就知道了!

AVL树

AVL树简介

1,AVL树概念:

简单来说就是,二叉搜索树里面任何一颗子树的左右子树高度差不超过1
名字由来:得名于它的发明者G. M. Adelson-Velsky和E. M. Landis是两个前苏联的科学家,

AVL树的实现

AVL树的结构

相比与我们之前实现的二叉搜索树,AVL树新增了:
1,指向父母的指针parent
2,平衡因子——bf(左右子树高度差,我们这里用右减左)

template<classK,classV>structAVLTreeNode{// 需要parent指针,后续更新平衡因子可以看到pair<K,V>_kv;AVLTreeNode<K,V>*_left;AVLTreeNode<K,V>*_right;AVLTreeNode<K,V>*_parent;int_bf;// balance factor};

我们可以发现,AVL树的每颗子树的平衡因子只能为1,-1,0

insert插入函数的实现⭐️⭐️⭐️⭐️⭐️

插入的过程很简单:首先还是跟二叉搜索树一样,先找到插入位置;
代码:和之前二叉搜索树时的一样

boolInsert(constpair<K,V>&kv){//插入已经有的值就会返回falseif(_root==nullptr){_root=newNode(kv);returntrue;}Node*cur=_root;Node*parent=nullptr;while(cur){if(kv.first>cur->_kv.first){parent=cur;cur=cur->_right;}elseif(kv.first<cur->_kv.first){parent=cur;cur=cur->_left;}else{returnfalse;}}

然后我们会发现,插入后会形成两种情况。

情况一是插入之后,它仍然是 AVL 树。
例如,在刚刚那副图里再插入一个11,仍然是AVV树

情况二是插入之后,它不满足 AVL 树的性质,这时候我们就要做出调整。
例如,插入13

好,我们一种情况一种情况来分析:
我们首先来想一下,我们要维护哪些东西:首先肯定是新增的那个平衡因子,还有父节点(parent),左右孩子,还有储存的值 kv。其中,这个平衡因子是比较难维护的。我们来单独看一下平衡因子怎么维护。

平衡因子的维护

首先,平衡因子是由右子树的高度减去左子树的高度得到的。

所以,如果一棵树在插入一个节点之后,它的左右子树高度都不变,那么它的平衡因子也不会改变。所以,平衡因子肯定跟高度有关。所以,在插入一个节点之后,该节点所有祖先节点的平衡因子都有可能受到影响,我们都需要进行更新。
但是,我们观察可以得出一个结论:
如果插入之后,有一棵子树它的根节点的平衡因子变为了 0,那么它的所有祖先节点的平衡因子都不用继续更新了
证明:
插入之后,它的平衡因子变为了 0。那么,插入之前,它的平衡因子肯定是1或者 -1。在是一和 -1 的时候,肯定是左右两边有一边多了一个,新增的那个元素就插入在了少的那一边,抹平了那个差距,但整体它的树的高度是没有变的
所以,那棵子树的高度是没有变的。即:插入之后平衡因子变为 0 的那棵子树,它的高度肯定是不会变的
那么,对于插入节点之后,父母的平衡因子变为 1 或 -1 的这种情况:
我们知道:它插入之前肯定是 0,插入后变为 1 或 -1,那么它的高度肯定是增加了 1

那么,接下来我们只需要看它是它父母的左子树还是右子树,根据它是它父母的左子树还是右子树来更新它父母的平衡因子。
第三种情况:插入之后,一直往上更新的时候,父节点的平衡因子变为了 2 或者 -2。那么这个情况比较复杂,就要利用到旋转来解决
好,那么我们就可以把插入之后的平衡因子进行归纳分类:

平衡因子的几种情况:

1,插入后是0:
不用继续向上更新
2,插入后是1,-1
根据是父母的左子树还是右子树,来更新父母的平衡因子
3,插入后是2,-2
情况比较多,要通过旋转来解决

我们先把前两种情况的代码写出来:

cur=newNode(kv);if(kv.first>parent->_kv.first){parent->_right=cur;parent->_bf++;}elseif(kv.first<parent->_kv.first){parent->_left=cur;parent->_bf--;}else{assert(false);//防御性编程,理论上不可能走到这里}cur->_parent=parent;cur->_bf=0;//更新平衡因子// 根据父母的平衡因子来移动//0,不用动//1,-1,不管是1还是-1,肯定是0变过来的,然后,肯定该子树的高度+1了,所以,看父母是父母的左孩子还是右孩子while(parent){if(parent->_bf==0)break;elseif(parent->_bf==1||parent->_bf==-1){cur=parent;parent=parent->_parent;if(parent==nullptr)break;//爷爷为空,那么就是到根节点了,直接breakif(parent->_left==cur){parent->_bf--;}elseif(parent->_right==cur){parent->_bf++;}elseassert(false);}

插入后,平衡因子为2和-2时

这里会分为四种情况,对应四种旋转方式,分别是:

  1. 左单旋
  2. 右单旋
  3. 左右双旋
  4. 右左双旋
    其中后面两个双旋就是上面两个单旋的组合,所以一定要先搞懂单选,再去看多选。搞懂单旋之后,双旋就会比较简单

右单旋

当一棵树它的左子树特别高(bf=-2)的时候,它就会采用单旋
下面这张图非常的关键!!

单从结果上来理解:

代码实现:

//所有旋转的情景是,元素已经插入,然后,超级不平衡,即parent的平衡因子=2/-2// 右单旋voidRotateR(Node*parent){//注意为空的几种情况// pParent为空// subLR 为空//Node*pParent=parent->_parent;Node*sub=parent;Node*subL=sub->_left;Node*subLR=subL->_right;sub->_left=subLR;if(subLR)//subLR可能为空,要特判subLR->_parent=sub;subL->_right=sub;sub->_parent=subL;if(pParent==nullptr){_root=subL;//parent也要更新!!!subL->_parent=nullptr;}elseif(pParent->_left==sub){pParent->_left=subL;subL->_parent=pParent;//别忘了更新parent}elseif(pParent->_right==sub){pParent->_right=subL;subL->_parent=pParent;}elseassert(false);//平衡因子更新subL->_bf=0;sub->_bf=0;}

左单旋

与右单旋刚好相反,它的右子树特别高(bf=2),所以要进行单旋。理解了右单旋,左单旋就很好理解了。

依旧是:把 parent 的右孩子作为新的根。

然后,parent 右孩子的左孩子裁剪下来,作为parent的右孩子,

最后,原来的 parent 作为新节点的左孩子。

左右双旋

顾名思义,“左右双旋”就是先进行一次左旋,再进行一次右旋
那么,我们就先来分析一下,到底是什么情况下要用单旋,什么情况下要用双旋。

其他两个同理:
左右单旋具体是怎么实现的呢?

简单来讲呢,双旋分为三种情况:

情况1:插入subLR的左子树

单从结果的角度来讲就是:

情况2:插入subLR的右子树(与情况一差不多)

情况3,特殊情况,h=0,即subLR就是插入节点


代码:

// 左右双旋,即先左旋在右旋voidRotateLR(Node*parent){Node*sub=parent;Node*subL=parent->_left;Node*subLR=subL->_right;intbf=subLR->_bf;//先存储一下,RotateL(subL);RotateR(sub);//更新平衡因子//subLR->_bf = 0;//这个节点成为新的根了,那么,肯定是0//其他两个,要根据插入节点是subLR的左右节点来判断//不能再rotate后根据平衡因子判断,因为这里已经变了!!!!!!!!// if (subLR->_bf == -1)//也就是插入图示里面的e,也就是8的左边if(bf==-1)//也就是插入图示里面的e,也就是8的左边{sub->_bf=1;subL->_bf=0;}elseif(bf==1){sub->_bf=0;subL->_bf=-1;}elseif(bf==0){sub->_bf=0;subL->_bf=0;}elseassert(false);subLR->_bf=0;//因为要以它为依据判断,所以,后更新}

右左双旋(同理,会了左右就会右左)

情况1

情况2

情况3,特殊情况,h=0

insert完整实现代码

// 插入boolInsert(constpair<K,V>&kv){//插入已经有的值就会返回falseif(_root==nullptr){_root=newNode(kv);returntrue;}Node*cur=_root;Node*parent=nullptr;while(cur){if(kv.first>cur->_kv.first){parent=cur;cur=cur->_right;}elseif(kv.first<cur->_kv.first){parent=cur;cur=cur->_left;}else{returnfalse;}}cur=newNode(kv);if(kv.first>parent->_kv.first){parent->_right=cur;parent->_bf++;}elseif(kv.first<parent->_kv.first){parent->_left=cur;parent->_bf--;}else{assert(false);//防御性编程,理论上不可能走到这里}cur->_parent=parent;cur->_bf=0;//更新平衡因子// 根据父母的平衡因子来移动//0,不用动//1,-1,不管是1还是-1,肯定是0变过来的,然后,肯定该子树的高度+1了,所以,看父母是父母的左孩子还是右孩子while(parent){if(parent->_bf==0)break;elseif(parent->_bf==1||parent->_bf==-1){cur=parent;parent=parent->_parent;if(parent==nullptr)break;//爷爷为空,那么就是到根节点了,直接breakif(parent->_left==cur){parent->_bf--;}elseif(parent->_right==cur){parent->_bf++;}elseassert(false);}elseif(parent->_bf==2||parent->_bf==-2){//旋转if(parent->_bf==-2&&cur->_bf==-1){RotateR(parent);//旋转后,不用向上更新了,break;}elseif(parent->_bf==-2&&cur->_bf==1){RotateLR(parent);break;}elseif(parent->_bf==2&&cur->_bf==1){RotateL(parent);break;}elseif(parent->_bf==2&&cur->_bf==-1){RotateRL(parent);break;}elseassert(false);}elseassert(false);}returntrue;}

左单旋代码

// 左单旋voidRotateL(Node*parent){Node*pparent=parent->_parent;Node*subR=parent->_right;Node*subRL=subR->_left;parent->_right=subRL;if(subRL)subRL->_parent=parent;subR->_left=parent;parent->_parent=subR;if(pparent==nullptr){_root=subR;//parent也要更新!!!subR->_parent=nullptr;}elseif(pparent->_left==parent){pparent->_left=subR;subR->_parent=pparent;}elseif(pparent->_right==parent){pparent->_right=subR;subR->_parent=pparent;}elseassert(false);//更新平衡因子parent->_bf=0;subR->_bf=0;}

右左双旋代码

voidRotateRL(Node*sub){Node*subR=sub->_right;Node*subRL=subR->_left;intbf=subRL->_bf;RotateR(subR);RotateL(sub);// 更新平衡因子if(bf==0){sub->_bf=0;subR->_bf=0;}elseif(bf==1)// 新节点插入在 subRL 的右边{sub->_bf=-1;// sub 变成了左子树,没有右孩子subR->_bf=0;// subR 左右平衡}elseif(bf==-1)// 新节点插入在 subRL 的左边{sub->_bf=0;// sub 左右平衡subR->_bf=1;// subR 只有右孩子}elseassert(false);subRL->_bf=0;}

IsBalanceTree判断一颗树是不是AVL树

计算右子树高度,计算左子树高度,然后相减。

判断差值的绝对值是否小于 2,以及该差值是否等于平衡因子 BF即可。
代码:

// 平衡检测辅助函数bool_IsBalanceTree(Node*root){if(root==nullptr)returntrue;intleft_height=_Height(root->_left);intright_height=_Height(root->_right);intbf=right_height-left_height;if(_IsBalanceTree(root->_left)&&_IsBalanceTree(root->_right)&&bf<2&&bf>-2){//还要判断平衡因子if(root->_bf!=bf){cout<<"平衡因子错误:"<<endl;returnfalse;}returntrue;}returnfalse;}

高度函数怎么求?

以前学过,就是递归:先递归左子树的高度,再递归右子树的高度,然后再加 1

总结:

全是重点!

下期预告

红黑树

结语

本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。
也欢迎订阅我的
深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++
算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线
快速复习系列:知识梳理、查漏补缺,考前冲刺必备
Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试


愿每一次敲下键盘,都比昨天更进一步!
愿每一行代码落下,都让未来多一种可能!
http://www.cnnetsun.cn/news/4210961.html

相关文章:

  • AI Agent五大核心设计模式详解:从ReAct到多智能体协作
  • AI智能体工程化实战:基于LangGraph构建多智能体协作系统
  • 锤子助手第019个开关:启用左滑返回的位置、验证方法与手势冲突边界
  • Java连接MySQL数据库时“Cannot load driver class”错误的全面排查与解决方案
  • 后端开发入门:先搞懂这些核心概念再说
  • 蓝速科技圆柱形 3D 全息舱硬件选型实战指南
  • JDK 21 --enable-preview 全链路配置指南:从编译到虚拟线程落地
  • 博图PLC硬件IO自由组态:用PEEK_BOOL/POKE_BOOL突破地址刚性限制
  • 红魔8S Pro强解Bootloader与完美ROOT实战指南
  • MySQL8.0.45主从搭建传统方式以及使用mysql clone克隆方式搭建
  • 企业终端外设管控难、漏洞多?一套闭环方案彻底解决
  • C++结构体排序:重载运算符、自定义函数与Lambda表达式实战指南
  • 从E-Bench到实战:构建面向真实场景的AI Agent评测基准
  • LLM智能体恒定上下文技能学习:从状态表示到工程实践
  • LLM智能体上下文污染:重试机制中的隐蔽陷阱与解决方案
  • 多模态AI智能体如何革新电影预演:从导演意图到可视化协作决策
  • GitLab项目群组设计与权限管理:从零构建清晰可扩展的代码仓库结构
  • LLM智能体在游戏中的竞争与合作:架构、策略与工程实践
  • SnapGuard:轻量级提示词注入防御方案,为视觉Web Agent构筑安全防火墙
  • OpenClaw智能体流量镜像重构:插件化设计与性能优化实践
  • Claude生成的pdf怎么导出 加上“AI导出鸭”,效果炸裂
  • 【TDengine】MNode、VNode、QNode、SNode 各自的职责是什么?
  • DMALibrary特征码扫描完全指南:如何在游戏中快速定位函数地址
  • AI编程实战:从工具应用到思维进化,资深开发者的人机协作指南
  • 基于腾讯云轻量服务器部署Moltbot AI助手:全链路安全防护实践
  • 从AI辅助到AI优先:构建智能研发流水线实现高频部署
  • 20+研究代码必备工具大清单:Good Research Code Handbook 全书工具索引与用途详解
  • JavaScript作用域与闭包讲解 - JavaScript学习系列文章
  • 深入解析AHB总线协议:SoC内部高速通信的核心机制与设计实践
  • 验证码技术演进:从字符识别到行为分析,开发者如何选择与集成