此文章为从二叉树到红黑树系列文章的第四节,主要介绍介绍二叉平衡搜索树AVL,当你理解了AVL,红黑树你就理解了一半了!
文章目录
一、前面文章链接~(点击右边波浪线可以返回目录)
在阅读本文前,强烈建议你看下前面的文章的目录、前言以及基本介绍,否则你无法理解后面的内容。链接如下:
二、由BST引入BBST~
在介绍AVL之前,我们先来了解下什么叫二叉平衡搜索树(BBST)以及为什么要引入BBST。
1.理想平衡~
既然二叉搜索树的性能主要取决于高度,故在节点数目固定的前提下,应尽可能地降低高度。
若树高恰好为⌊log2n⌋(二叉树的最低高度),则称作理想平衡树。如满二叉树和完全二叉树。
遗憾的是,完全二叉树 “叶节点只能出现于最底部两层” 的限制过于苛刻。所以要制定某种宽松的标准来实现适度平衡。
2.适度平衡~
将树高限制为“渐进地不超过log2n”。如AVL,RedBlack等。
3.等价变换~
若两棵树的中序遍历序列相同,则彼此等价。
三、AVL树的定义~
1.AVL树的适度平衡~
2.AVL树的平衡因子~
由适度平衡的定义不难得知,要保持一颗AVL树的平衡,必须使其左右子树的高度相差不超过1.故用数学的方式可以理解为:
四、AVL类~
(一)定义变量和接口~
1.利用已有的成员变量~
由于AVL树属于BST的一种,因此AVL树可以继承自BST,而BST又继承自BinTree(二叉树),而在此前(本系列文章第2节,第3节中),我们已经拥有了以下成员变量,因此不需要额外给AVL定义变量。
BinNodePtr _hot;//"命中节点"的"父亲"
int _size;//二叉树的规模
BinNodePtr _root;//二叉树的树根
2.需要的接口~
由于在BST中,我们已经定义了查找search算法,因此,不需要给AVL重新写查找算法,只需要对插入和删除算法进行重写既可,AVL和BST的插入和删除算法,本质上没有区别,有区别的仅仅是插入和删除之后的调整,而这种调整正是AVL与BST的最大差别所在。
在树中插入一个节点insert
在树中删除一个节点remove
3.重要辅助函数~
在AVL中,引入了一个平衡因子的概念,所以需要一个函数,来判断此时的AVL树是否平衡。
判断是否平衡的函数IsAvlBalanced
并且还需要一个重要的辅助函数来得到当前节点的最高的那个孩子节点(这个函数在描述AVL的插入删除算法时就会发挥作用)
得到该节点更高的孩子tallerChild
此外,不要忘了在BST中遗留的两个函数,在介绍重平衡时,也会描述这两个算法的作用
3+4重构 connect34
对该节点及其父亲、祖父做统一旋转调整rotateAt
4.AVL.h~
template<typename T = int>
class AVL :public BST<T> {
protected:
using BinNodePtr = BinNode<T>*;
public:
BinNodePtr insert(const T& data)override;//插入(重写)
bool remove(const T& data)override;//删除(重写)
protected:
static constexpr bool IsAvlBalanced(const BinNodePtr& x) {
//判断是否平衡
int BalanceFactor = stature(x->_lchild) - stature(x->_rchild);
return (-2 < BalanceFactor && BalanceFactor < 2);
}
static BinNodePtr& tallerChild(BinNode<T>*& x);//更高的孩子
};//class AVL
(二)判断是否失衡~
借助AVL树的平衡因子的概念,不难写出判断是否平衡的代码。其中stature是在本系列文章第一部分定义的得出当前节点高度的全局静态函数。
static constexpr bool IsAvlBalanced(const BinNodePtr& x) {
//判断是否平衡
int BalanceFactor = stature(x->_lchild) - stature(x->_rchild);
return (-2 < BalanceFactor && BalanceFactor < 2);
}
AVL树失衡时,当且仅当IsAvlBalanced函数的返回值为false。
(三)AVL的失衡与重平衡~
当AVL的平衡因子不再满足平衡条件时,AVL就会发生失衡,而失衡无外乎就四种失衡情况,接下来,我们通过具体的例子来看看因为插入和删除引起的AVL的失衡。
1.因插入引起的失衡~
(1)LL型失衡~
下图5 3由于2的插入,导致5的失衡,所以进行重平衡,即将3提升为根节点,5作为3的右孩子。
(2)RR型失衡~
下图2 3由于4的插入,导致2的失衡,所以进行重平衡,即将3提升为根节点,2作为3的左孩子。
(3)LR型失衡~
下图3 1由于2的插入,导致3的失衡,所以进行重平衡,即将2提升为根节点,1作为2的左孩子,3作为2的右孩子。
(4)RL型失衡~
下图3 5由于4的插入,导致3的失衡,所以进行重平衡,即将4提升为根节点,3作为4的左孩子,5作为4的右孩子。
2.因删除引起的失衡~
(1)LL型失衡~
下图3 2 5 1由于5的插入,导致3的失衡,所以进行重平衡,即将2提升为根节点,3作为2的右孩子。
(2)RR型失衡~
下图3 2 4 5由于2的删除,导致3的失衡,所以进行重平衡,即将4提升为根节点,3作为4的左孩子。
(3)LR型失衡~
下图4 2 5 3由于5的删除,导致4的失衡,所以进行重平衡,即将3提升为根节点,2作为3的左孩子,4作为3的右孩子。
(4)RL型失衡~
下图2 1 5 3由于1的删除,导致2的失衡,所以进行重平衡,即将3提升为根节点,2作为3的左孩子,5作为3的右孩子。
3.失衡情况总结~
从上面插入的四种情况,以及删除的四种情况可以看出,无论是插入还是删除,其失衡之后的形状都是一样的,所以调整的方式也是一样的!
因此,不管是插入还是删除,我们都可以采用同样的调整方式。
在很多教程上,都是用单旋(LL 或RR型失衡 只旋转一次)和双旋(LR或RL型失衡 要旋转两次)来处理失衡的情况,如果你看过其他的教程,那么你必然对下面的图有所熟悉。当然,你不熟悉,也没关系,我们只看这四种情况对应的结果。
图来自维基百科
可以发现,无论是单旋还是双旋,其调整之后的形状毫无例外,都是这种结构。
所以,我们可以只关注结果,不关心过程,无论哪种失衡情况,其调整之后的结构都是上面这种结构。因此就可以写一个通用的调整函数,来专门负责失衡之后的调整。
4.统一重平衡算法~
幸运的是,邓老师已经给出了解决方案。有了这种方法,你就再也不必为到底该左旋还是右旋而苦恼,也不必去记那种繁琐的左右旋算法。
(1)万能的connect 3+4算法~
将上面的四种情况,统一简化成下面图中的形式。无论是那种调整方式,最终都必然调整为这种形式。
实际上,这一理解涵盖了所有的单旋和双旋情况。相应的重构过程,仅涉及局部的三个节点及其四棵子树,故称作“3 + 4”重构。
到这里,我相信你已经理解了在第三部分BST中所定义的connect34函数的作用了。
connece34代码~
接下来只要将对应的节点按顺序填入下面这个函数,就能实现重平衡。同时注意更新高度。
在BST.h中定义的成员函数connect34,注意返回值为调整之后的根节点。
template<typename T>
BinNode<T>* BST<T>::connect34(
BinNode<T>* a, BinNode<T>* b, BinNode<T>* c,
BinNode<T>* T1, BinNode<T>* T2, BinNode<T>* T3, BinNode<T>* T4)
{
a->_lchild = T1; if (T1)T1->_parent = a;
a->_rchild = T2; if (T2)T2->_parent