👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
前言
在搜索二叉树章节,我们知道二叉搜索树可能会失去平衡(退化成单支树),造成搜索效率低落的情况,时间复杂度会退化成
O(N)
(效率没有保障)。因此,一些大佬在搜索二叉树的基础上增加了平衡的性质,于是就诞生了AVL
树、红黑树等平衡树。
目录
一、AVL树的概念
为了解决二叉搜索树的缺陷,两位俄罗斯的数学家G.M.Adelson-Velskii
和E.M.Landis
发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1
,即可降低树的高度,确保整颗树的深度是O(logN)
。
因此,如果二叉搜索树具备以下性质:
- 它的左右子树都是
AVL
树 右子树的高度 - 左子树的高度
的绝对值不超过1
(平衡因子)
那么这颗搜索二叉树就是一颗AVL
树。
二、AVL树的定义
AVL
树在原二叉搜索树的基础上添加了平衡因子(Balance factor
),简写: bf
,以及用于快速向上调整的父亲指针parent
(为什么定义指针变量parent
,在插入部分会介绍到),所以AVL
树是一个三叉链结构。
至于AVLTree
类中,成员变量只需要创建一个 根节点_root
即可
注意: 本篇博客规定平衡因子差值为右 - 左
。当然左 - 右
也是可以的,根据自己的个人习惯。
三、如何更新平衡因子
通过上图,我们可以发现一个结论: 左边多一个结点,其祖先的路径上的平衡因子_bf--
。
同理的,右边多一个结点,其祖先的路径上的平衡因子_bf++
。
【总结】
- 左边多一个结点,其祖先的路径上的平衡因子
_bf--
。- 右边多一个结点,其祖先的路径上的平衡因子
_bf++
。
四、如何控制平衡
将结点插入以后,我们需要做的就是控制平衡。
因此,就有以下3
种情况
- 当
parent
的平衡因子为0
时,说明插入的结点已经把矮的那边给补上了,那么就已经绝对平衡了,没必要再沿着祖先向上更新。
- 当
parent
的平衡因子为1
或者-1
,就要沿着祖先的路径向上检查是否要更新。(祖先可能会不平衡)
- 当
parent
的平衡因子为2
或者-2
,说明不平衡。解决方法:对parent
所在的子树进行旋转。(具体后面再谈)
根据以上分析,我们就可以写出大致的AVL
插入的框架
解答为需要定义parent指针变量
因此,定义parent
指针变量就是为了快速找到某结点的父亲,从而快速更新平衡因子以及快速判断是否需要进行旋转操作,减低了遍历子树来找到父结点的时间。
五、插入操作(重点)
旋转需要注意的问题
-
还是需要保持它是一颗具有搜索树的性质(左子树比根小,右子树比根大)
-
让它变成平衡树,且减低这个子树的高度
5.1 左单旋
左单旋是为了处理当某个结点的右子树过深而导致失衡的情况(parent->_bf == 2 && cur->_bf == 1
)。具体步骤如下:
-
将
cur
的左结点作为parent
的右结点。 -
再将
parent
结点作为cur
的左结点。 -
修改父亲关系
cur
左结点的父亲就是parent
parent
结点的父亲就是cur
-
最后要考虑
cur
是否是以子树形成存在的,还是它就是个_root
【代码实现】
【左旋转代码实现】
5.2 右单旋
右单旋本质上和左单旋一样,有种对称的感觉~
右单旋是为了处理当某个节点的左子树过深而导致失衡的情况(parent->_bf == -2 && cur->_bf == -1
)。具体步骤如下:
-
将
cur
的右结点作为parent
的左结点。 -
再将
parent
结点作为cur
的右结点。 -
修改父亲关系
cur
的右结点的父亲就是parent
parent
结点的父亲就是cur
-
最后要考虑
cur
是否是以子树形成存在的,还是它就是个_root
【代码实现】
【右单旋代码实现】
5.3 双旋之左右双旋
从上图A
样例发现:parent
的左子树高,因此很容易可以想到右单旋来控制平衡。但是,通过图B
发现,右单旋还是解决不了问题。
那么,如果是以上这种 折线型
不平衡的情况,则要使用双旋来解决。
左右双旋是为了处理parent
的左子树cur
的右子树过深而导致失衡的情况(parent->_bf == -2 && cur->_bf == 1
)。具体步骤如下:
- 先对
cur
结点进行左旋操作(因为cur
右子树过深) - 最后对
parent
结点进行右旋操作
我们可以根据以上步骤来验证
通过以上分析,有的人可能会旋转代码复用,几行代码就搞定了。
如果这样写就错了,**如果先左旋的话,左旋函数会自动将cur
和parent
的平衡因子置为0
,然而左旋后,还要通过右旋才能平衡,因此 需要考虑旋转后平衡因子的情况:
-
当
curRight
的平衡因子为0
时(没有孩子),左右双旋后parent
、cur
、curRight
平衡因子都为0
-
当
curRight
的平衡因子为-1
时(有左孩子),左右双旋后parent
、cur
、curRight
平衡因子分别为1
、0
、0
-
当
curRight
的平衡因子为1
时(有右孩子),左右双旋后parent
、cur
、curRight
平衡因子分别为0
、-1
、0
【代码实现】
【左右双旋代码实现】
5.4 双旋之右左双旋
右左双旋是为了处理parent
结点的右结点cur
的左子树过深而导致失衡的情况(parent->_bf == 2 && cur->_bf == -1
)。具体步骤如下:
- 先对
cur
结点进行右旋操作(cur
左子树过深) - 最后再对
parent
结点进行左旋操作
右左旋和左右旋类似,手撕代码之前同样需要考虑旋转后平衡因子的情况:
- 当
curLeft
的平衡因子为0
时(没有孩子),右左双旋后parent
、cur
、curLeft
平衡因子都为0
- 当
curLeft
的平衡因子为-1
时(有左孩子),右左双旋后parent
、cur
、curLeft
平衡因子分别为都为0
、1
、0
- 当
curLeft
的平衡因子为1
时(有右孩子),右左双旋后parent
、cur
、curLeft
平衡因子分别为都为-1
、0
、0
【代码实现】
【右左双旋代码实现】
六、 AVL树的验证
平衡因子反映的是左右子树高度之差(本篇博客是:右子树 - 左子树
)。通过计算出左右子树高度之差并与当前节点的平衡因子进行比对,如果发现不同,则说明 AVL
树非法。
注意:如果当前节点的 平衡因子 取值范围不在[-1, 1]
内,也可以判断非法
// 验证AVL树
int Height(Node* root)
{
if (root == nullptr)
{
return 0;
}
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
bool IsBalance()
{
return IsBalance(_root);
}
bool IsBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf
|| root->_bf < -1
|| root->_bf > 1)
{
cout << "平衡因子异常:" << root->_key.first << "->" << root->_bf << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
通过一段简单的代码,随机插入1w
个节点,判断是否合法
#include "AVL.h"
#include <vector>
int main()
{
const int N = 10000;
vector<int> v(N);
srand((size_t)time(NULL));
for (int i = 0; i < N; i++)
{
v.push_back(rand());
}
AVLTree<int, int> t;
for (auto x : v)
{
t.insert(make_pair(x, x));
cout << "Insert:" << x << "->" << t.IsBalance() << endl;
}
return 0;
}
当打印出来的结果全为1
(表示真),那么它就是一个AVL
树
七、总结
AVL
树是一棵 绝对平衡 的二叉树,对高度的控制极为苛刻,稍微有点退化的趋势,都要被旋转调整,这样做的好处是 严格控制了查询的时间,查询速度极快,时间复杂度为 logN
。而对于删除,大家不用担心,因为在面试的时候只会考察AVL
树的插入操作hh
这是本篇博客的相关代码:代码仓库。