
⭐️博主: 此生决int-@CSDN博客
** 速胜派就是最大的投降派!!!**
** 🔥热门专栏🔥**
文章目录
上期回顾
上一篇我们主要学习了 如何使用map和set,了解了他们相关的接口,做了相关的一些算法题,那么今天,我们就来看看,怎么保证二叉搜索树的高度不会太高,达到logN的效率的呢?那要我们学完今天的AVL树就知道了!,
AVL树
AVL树简介
1,AVL树概念:
简单来说就是,二叉搜索树里面任何一颗子树的左右子树高度差不超过1
名字由来:得名于它的发明者G. M. Adelson-Velsky和E. M. Landis是两个前苏联的科学家,
AVL树的实现
AVL树的结构
相比与我们之前实现的二叉搜索树,AVL树新增了:
1,指向父母的指针parent
2,平衡因子------bf(左右子树高度差,我们这里用右减左)
cpp
template<class K, class V>
struct AVLTreeNode
{
// 需要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插入函数的实现⭐️⭐️⭐️⭐️⭐️
插入的过程很简单:首先还是跟二叉搜索树一样,先找到插入位置;
代码:和之前二叉搜索树时的一样
cpp
bool Insert(const pair<K, V>& kv)
{
//插入已经有的值就会返回false
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent= nullptr;
while (cur)
{
if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
然后我们会发现,插入后会形成两种情况。
情况一是插入之后,它仍然是 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
情况比较多,要通过旋转来解决
我们先把前两种情况的代码写出来:
cpp
cur = new Node(kv);
if (kv.first>parent->_kv.first)
{
parent->_right = cur;
parent->_bf++;
}
else if (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;
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
if (parent == nullptr)break;//爷爷为空,那么就是到根节点了,直接break
if (parent->_left == cur)
{
parent->_bf--;
}
else if (parent->_right == cur)
{
parent->_bf++;
}
else
assert(false);
}
插入后,平衡因子为2和-2时
这里会分为四种情况,对应四种旋转方式,分别是:
- 左单旋
- 右单旋
- 左右双旋
- 右左双旋
其中后面两个双旋就是上面两个单旋的组合,所以一定要先搞懂单选,再去看多选。搞懂单旋之后,双旋就会比较简单。
右单旋
当一棵树它的左子树特别高(bf=-2) 的时候,它就会采用右 单旋
下面这张图非常的关键!!

单从结果上来理解:

代码实现:
cpp
//所有旋转的情景是,元素已经插入,然后,超级不平衡,即parent的平衡因子=2/-2
// 右单旋
void RotateR(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;
}
else if (pParent->_left == sub)
{
pParent->_left = subL;
subL->_parent = pParent;//别忘了更新parent
}
else if (pParent->_right == sub)
{
pParent->_right = subL;
subL->_parent = pParent;
}
else
assert(false);
//平衡因子更新
subL->_bf = 0;
sub->_bf = 0;
}
左单旋
与右单旋刚好相反,它的右子树特别高(bf=2) ,所以要进行左 单旋。理解了右单旋,左单旋就很好理解了。

依旧是:把 parent 的右孩子作为新的根。
然后,parent 右孩子的左孩子裁剪下来,作为parent的右孩子,
最后,原来的 parent 作为新节点的左孩子。
左右双旋
顾名思义,"左右双旋"就是先进行一次左旋,再进行一次右旋
那么,我们就先来分析一下,到底是什么情况下要用单旋,什么情况下要用双旋。

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

简单来讲呢,双旋分为三种情况:
情况1:插入subLR的左子树

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

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

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

代码:
cpp
// 左右双旋,即先左旋在右旋
void RotateLR(Node* parent)
{
Node* sub = parent;
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = 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;
}
else if (bf == 1)
{
sub->_bf = 0;
subL->_bf = -1;
}
else if (bf == 0)
{
sub->_bf = 0;
subL->_bf = 0;
}
else
assert(false);
subLR->_bf = 0;//因为要以它为依据判断,所以,后更新
}
右左双旋(同理,会了左右就会右左)
情况1

情况2

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

insert完整实现代码
cpp
// 插入
bool Insert(const pair<K, V>& kv)
{
//插入已经有的值就会返回false
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent= nullptr;
while (cur)
{
if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
if (kv.first>parent->_kv.first)
{
parent->_right = cur;
parent->_bf++;
}
else if (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;
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
if (parent == nullptr)break;//爷爷为空,那么就是到根节点了,直接break
if (parent->_left == cur)
{
parent->_bf--;
}
else if (parent->_right == cur)
{
parent->_bf++;
}
else
assert(false);
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//旋转
if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
//旋转后,不用向上更新了,
break;
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent); break;
}
else if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent); break;
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent); break;
}
else
assert(false);
}
else
assert(false);
}
return true;
}
左单旋代码
cpp
// 左单旋
void RotateL(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;
}
else if (pparent->_left == parent)
{
pparent->_left = subR;
subR->_parent = pparent;
}
else if (pparent->_right == parent)
{
pparent->_right = subR;
subR->_parent = pparent;
}
else
assert(false);
//更新平衡因子
parent->_bf = 0;
subR->_bf = 0;
}
右左双旋代码
cpp
void RotateRL(Node* sub)
{
Node* subR = sub->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(subR);
RotateL(sub);
// 更新平衡因子
if (bf == 0)
{
sub->_bf = 0;
subR->_bf = 0;
}
else if (bf == 1) // 新节点插入在 subRL 的右边
{
sub->_bf = -1; // sub 变成了左子树,没有右孩子
subR->_bf = 0; // subR 左右平衡
}
else if (bf == -1) // 新节点插入在 subRL 的左边
{
sub->_bf = 0; // sub 左右平衡
subR->_bf = 1; // subR 只有右孩子
}
else
assert(false);
subRL->_bf = 0;
}
IsBalanceTree判断一颗树是不是AVL树
计算右子树高度,计算左子树高度,然后相减。
判断差值的绝对值是否小于 2,以及该差值是否等于平衡因子 BF即可。
代码:
cpp
// 平衡检测辅助函数
bool _IsBalanceTree(Node* root)
{
if (root == nullptr)return true;
int left_height =_Height(root->_left);
int right_height = _Height(root->_right);
int bf = right_height - left_height;
if (_IsBalanceTree(root->_left) && _IsBalanceTree(root->_right) && bf<2 && bf>-2)
{
//还要判断平衡因子
if (root->_bf!= bf)
{
cout << "平衡因子错误:" << endl;
return false;
}
return true;
}
return false;
}
高度函数怎么求?
以前学过,就是递归:先递归左子树的高度,再递归右子树的高度,然后再加 1
总结:
全是重点!
下期预告
红黑树
结语
** 本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**
** 也欢迎订阅我的**
深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++
算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线
快速复习系列:知识梳理、查漏补缺,考前冲刺必备
Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试
** 愿每一次敲下键盘,都比昨天更进一步!** ** 愿每一行代码落下,都让未来多一种可能!**