news 2026/8/25 12:14:19

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解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,轻松备战期末考试


愿每一次敲下键盘,都比昨天更进一步!
愿每一行代码落下,都让未来多一种可能!
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/25 12:04:41

AI Agent五大核心设计模式详解:从ReAct到多智能体协作

这次我们来看一个关于AI Agent设计模式的技术话题。如果你正在开发或研究AI Agent&#xff0c;想知道如何让智能体更稳定、更高效地工作&#xff0c;那么理解其核心设计模式是关键。本文不会空谈概念&#xff0c;而是直接切入五种最核心、最实用的AI Agent设计模式&#xff0c;…

作者头像 李华
网站建设 2026/8/25 12:03:12

AI智能体工程化实战:基于LangGraph构建多智能体协作系统

大家好&#xff0c;我是专注于技术实战分享的博主。在探索AI工程化落地的过程中&#xff0c;我们常常面临一个核心挑战&#xff1a;如何将前沿的AI能力&#xff0c;特别是智能体&#xff08;Agents&#xff09;&#xff0c;有效地整合到现有的软件工程流程中&#xff1f;这不仅…

作者头像 李华
网站建设 2026/8/25 11:55:27

后端开发入门:先搞懂这些核心概念再说

你第一次写后端接口时&#xff0c;可能以为后端就是接收请求、查数据库、返回JSON。等你真正踏入生产环境&#xff0c;才发现这套想象只覆盖了冰山一角。后端开发入门最大的误区&#xff0c;就是先学框架和语法&#xff0c;而不是先理解那些与语言无关的核心概念。后端是一门关…

作者头像 李华
网站建设 2026/8/25 11:53:16

蓝速科技圆柱形 3D 全息舱硬件选型实战指南

在智慧展厅或政务大厅的规划阶段&#xff0c;大家往往容易陷入一个误区&#xff1a;过度关注数字人画面的炫酷程度&#xff0c;却忽略了承载这些内容的硬件本体。实际上&#xff0c;对于需要 724 小时连续运行的商用项目而言&#xff0c;整机硬件的“底子”才是决定项目口碑的关…

作者头像 李华