AVL树的插入
- 前言
- 左单旋
- 右单旋
- 左右双旋
- 右左双旋
- 检查是否这颗树是否是AVL树
前言
AVL树可以说是对二叉搜索树的优化,我们来看二叉树搜索树的下一面一种特殊情况:
当我们插入的数是上面的情况时,二叉树搜索树的特点就形同虚设了,这就相当于一个长度为N的单链表了,那么时间复杂度就是O(N)了。
那么AVL树为了处理这种情况的发生呢,就应运而生。
先来大致的说一下AVL是怎么处理才能避免出现这种情况的。AVL树主要是通过几种旋转的方式,控制了整颗树的任意结点的高度差不超过1,如果超过1,就会发生旋转。
AVL树的定义:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树 。
- 左右子树高度之差(简称平衡因子)的绝对值不超过1。
AVL树的结点定义:
在上面提到了平衡因子是左右子树的高度差的绝对值,为了代码的实现的更明确,我们采用右子树的高度减去左子树的高度
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
pair<K, V> _kv;
int _bf;//平衡因子(该结点的左右子树的高度差),右子树的高度减去左子树的高度
AVLTreeNode(const pair<K, V>& kv)
: _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{}
};
因为在高度不满足条件的时候,涉及到旋转,而旋转就必须要有一个指针指向前一个结点,所以我们在定义结点的时候,将结点定义成了三叉链的结构。
再提一句就是关于这个pari<K, V> _kv。我们在实现的时候,完全可以根据自己的场景需要来决定是否使用pair。如果你仅仅需要一个值的话,是可以只提供一个模板参数的。
在正式的谈旋转之前,我们要先将能触发旋转的场景全部分析出来,主要的就是四种大场景。
其中左单旋和右单旋都是比较简单的。
左单旋
那么下面我们就用画图的方式来看一下如何触发左单旋。
上图的a b c矩形代表的是各种满足AVL的结点的情况,因为不同的情况有很多很多种,所以这里就以抽象图来观察我们的旋转的情况,这种抽象图也是大佬在观察了很多种情况总结出来的。
在上图中,当我们要在c结点插入一个新结点的时候(先不用考虑增加b的情况,这种情况是会引用发更为复杂的旋转的情况,下面会具体分析
),c的高度就会增加1,变成h+1高度。c结点的高度的变化会直接影响其父亲结点的高度差,也就是60结点的高度差会由0变成1。60的高度的更新,又会影响其父亲结点的高度差的变化,当c的高度增加1的时候,增颗右子树的高度就会由h+1变成h+2,这时根节点30的左右子树的高度差就由1变成了2。
如下图所示:
此时根节点的高度差不满足绝对值小于1了,这时候的解决办法就是左旋转一下。
过程如下:
- 将60的左子树给30做右子树。
- 30做60的左子树。
结果如下图所示:
11e4c0b9cd9.png)
大家可以想一下为什么可以这样做?提示:二叉搜索树的特点是什么?
旋转完成之后,30和60的平衡因子需要再重新更新一下。
代码如下:
parent就是引发旋转的那个结点,上图中就是30。
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//1.
parent->_right = subRL;
subR->_left = parent;
//2.更改parent指向
if (subRL != nullptr)
subRL->_parent = parent;
Node* ppNode = parent->_parent;//在更改parent的_parent之前记录下parent的父结点
parent->_parent = subR;
//3.还需要进行判断,该parent是否是整个树的根节点还是子树的根节点
if (ppNode == nullptr)
{
//parent是整个树的根节点
_root = subR;
subR->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
ppNode->_left = subR;
else
ppNode->_right = subR;
subR->_parent = ppNode;
}
//4.更新平衡因子
subR->_bf = parent->_bf = 0;
}
大家把变量名带上再画一张旋转的图就更容易理解代码的逻辑。
右单旋
右单旋其实和左单旋可以说是完全对称的结构,下面给出右单旋的所示图,过程就不再解释了,和左单旋并没有什么本质上的区别。
旋转完成之后,记得更新平衡因子的值。
以下是代码:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//1.
parent->_left = subLR;
subL->_right = parent;
//2.更改parent指向
if (subLR != nullptr)
subLR->_parent = parent;
Node* ppNode = parent->_parent;
parent->_parent = subL;
//3.还需要进行判断,该parent是否是整个树的根节点还是子树的根节点
if (ppNode == nullptr)
{
//parent是整个树的根节点
_root = subL;
subL->_parent = nullptr;
}
else
{
if(ppNode->_left == parent)
ppNode->_left = subL;
else
ppNode->_right = subL;
subL->_parent = ppNode;
}
//4.更新平衡因子
subL->_bf = parent->_bf = 0;
}
下面是两种单旋图的所示图,放在一起对比大家再来看,是真的没什么本质的区别:
左右双旋
接下来的双旋的场景,说难也不难,说简单也不简单哈哈!
还是同样的我们以抽象图来画图,这里提一句大家可以将抽象图画成具体的实例图去走旋转,只不过实例图的场景实在是太多,不太适合拿出来举例子,也不具有说服力,这四种旋转的抽象图就是包含了各种场景的四种分类。
如果不理解直接记住也行的。
还是先给出一颗AVL树,如下所示:
此时如果我们在60结点的左子树或者右子树下插入新的结点,引发的高度差变化就如下图所示:
(在b或者c插入引发的是同一种双旋,但是虽然是同一种双旋,但是会导致最后的更新的平衡因子不同,所以在进行旋转之前我们要事先记录下,60结点的平衡因子,在后序的更新平衡因子的时候,我们会用到这一点
)
这时候的根节点的做右子树的高度差不再满足条件,就会触发旋转,这种场景仅仅一次单旋肯定是没法解决的,所示我们就需要双旋了。
第一次单旋:先对30结点进行左单旋,结果如下图所示:
第二次单旋:对90结点进行右单旋,结果如下图所示:
左旋和右旋的规则上面已经介绍过了,可以看出,双旋也就是两次单旋组合的,只不过在最后的旋转完成之后,需要对一些结点的平衡因子进行更新,更新平衡因子和单旋的时候不一样,所示在实现双旋的代码的时候,对于旋转,只需要复用之前写的单旋的代码,自己最后写一下关于平衡因子的更新即可。
代码如下:
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;//根据这个结点的平衡因子来确定新形成的AVL树的平衡因子
RotateL(parent->_left);
RotateR(parent);
//无论那种情况,subL(根)的平衡因子都是0
subLR->_bf = 0;
if (bf == 0)
{
parent->_bf = subL->_bf = 0;
}
else if (bf == -1)
{
//结合画抽象图来看平衡因子怎么更新!!!!!!!!!!!!!!!!!--易错点
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
subL->_bf = -1;
parent->_bf = 0;
}
else
{
assert(false);
}
}
右左双旋
右左双旋和左右双旋的关系呢,和左单旋和右单旋的关系是一样的,也是对称的关,只不过最后的平衡因子的更新还是需要根据画图来进行判断。
以下是右左双旋的过程示意图:
代码:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
subRL->_bf = 0;
if (bf == 0)
{
parent->_bf = subR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
}
else if (bf == 1)
{
parent->_bf =-1;
subR->_bf = 0;
}
else
assert(false);
}
左右双旋和右左双旋的对比示意图:
检查是否这颗树是否是AVL树
最后我们需要写一个函数,来检查用我们上面实现出来的 AVL树是满足要求的,即是一颗正确的AVL树。
值得一提的是,我们不能使用平衡因子来检查这颗树是否是正确的AVL树,因为平衡因子的更新也是我们自己写的,如果说因为我们自己的写的平衡因子有错误,而没有检查出来这颗树是有问题的,那么就无法保证改树是AVL树了。
思路如下:
既然平衡因子用不了,那么我们可以考虑直接计算出每个结点的左右子树的高度差,直接通过高度差来判断每个结点的高度差是否小于2。如果说不满足,那么就是我们实现的有问题,如果全部都检查完了,就说明我们实现的代码是能够构造出一颗正确的AVL树的。
代码如下:
bool isBalance()
{
return _isBalance(_root);
}
private:
bool _isBalance(Node* root)
{
if (root == nullptr)
return true;
int leftHT = Height(root->_left);
int rightHT = Height(root->_right);
int diff = rightHT - leftHT;
if (diff != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
//所有的结点都满足AVLTree这颗树才是AVLTree
return abs(diff) < 2
&& _isBalance(root->_left)
&& _isBalance(root->_right);
}
int Height(Node* root)
{
if (root == nullptr)
return 0;
return max(Height(root->_left), Height(root->_right)) + 1;
}
测试用例及结果:
测试函数返回结果为真,结果正确。!
以下是完整的AVL树代码链接:
https://gitee.com/WXK-Tom/c-data-structure/tree/master/AVLTree/AVLTree
欢迎大家评论留言!