此文章为从二叉树到红黑树系列文章的第四节,主要介绍介绍二叉平衡搜索树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 = a; this->updateHeight(a);
c->_lchild = T3; if (T3)T3->_parent = c;
c->_rchild = T4; if (T4)T4->_parent = c; this->updateHeight(c);
b->_lchild = a; a->_parent = b;
b->_rchild = c; c->_parent = b; this->updateHeight(b);
return b;
}
下一步,问题就在于,如何确定 a b c T1 T2 T3 T4的顺序?
(2)万能的rotateAt算法~
观察上面四种失衡情况,无论是哪种失衡情况,其失衡时,都必然涉及3个节点的位置变换。
并且这三个节点,也一定为祖孙三代的关系。
因此,不妨设最小的那个节点为v,其父亲为p,其祖父为g。分四种情况,将v p g以及对应的孩子,放入connect34函数里面进行重平衡。
rotateAt代码~
在BST.h中,我们定义了rotateAt算法,下面为其具体实现。对需要调整的节点,分四种情况,借助connect34函数,进行调整7个节点的相对位置。
template<typename T>
BinNode<T>* BST<T>::rotateAt(BinNode<T>* v) //返回调整后局部子树根节点的位置
{
if (v == nullptr) {
printf("Error!"); exit(0);
}
//设定v的父亲与祖父//视v、p和g相对位置分四种情况
BinNode<T>* p = v->_parent; BinNode<T>* g = p->_parent;
if (IsLChild(p)) {//L
if (IsLChild(v)) {//LL
p->_parent = g->_parent;//向上连接
return connect34(v, p, g, v->_lchild, v->_rchild, p->_rchild, g->_rchild);
}
else {//LR
v->_parent = g->_parent;//向上连接
return connect34(p, v, g, p->_lchild, v->_lchild, v->_rchild, g->_rchild);
}
}
else {//R
if (IsRChild(v)) {//RR
p->_parent = g->_parent;//向上连接
return connect34(g, p, v, g->_lchild, p->_lchild, v->_lchild, v->_rchild);
}
else {//RL
v->_parent = g->_parent;//向上连接
return connect34(g, v, p, g->_lchild, v->_lchild, v->_rchild, p->_rchild);
}
}
}
读者可以继续通过这两个图来细细体会。
(四)AVL的插入~
在理解了AVL的重平衡原理之后,AVL的插入和删除就也很容易理解了。
在插入新的节点的时候,有一个现象,就是无论如何:
1. 新节点的父亲节点,一定不可能失衡;
2. 并且失衡的节点一定为该新节点的祖先节点;
3. 一旦其有一个祖先恢复了平衡,整颗AVL树必将恢复平衡。
插入代码~
像往常的树的插入一样,在插入之前,我们需要进行查找操作,如果存在这个节点,就不插入,如果不存在这个节点,就插入新的节点,并且得益于_hot节点(见第三部分BST中_hot节点的作用),我们能很快的将节点插入正确的位置,同时也更新了对应的父子节点指针。
接下来就按照AVL插入新节点的性质,来写出下面的代码。并且在此处我们要用到FromParentTo算法(见第二部分BinTree),此算法的作用是获取当前节点的父亲的孩子的引用,即获取当前节点的引用,哪怕当前节点里面的内容发生了变化,其作为指针的本身的地址不会发生变化,只是指针所指的地址发生了变化。
template<typename T>
BinNode<T>* AVL<T>::insert(const T& data){//无论data在不在子树中,返回值的_data均为data
BinNode<T>*& x = this->search(data);
if (x)
return x;
x = new BinNode<T>(data,this->_hot);//创建新节点
this->_size++;//更新规模
for (BinNode<T>* g = this->_hot; g; g = g->_parent) {
if (!IsAvlBalanced(g)) {//如果失衡
//必须要提前将地址取好,不然g的里面的数据发生变动,就会造成指针乱指。
BinNode<T>*& parent_child_Address = this->FromParentTo(g);
parent_child_Address= this->rotateAt(tallerChild(tallerChild(g)));
/*首先调用tallerChild函数,来确定到底旋转哪一个g的孙子节点(选择g的最高孩子的最高孩子作为旋转节点)
*在旋转过程中,对g的孩子,g的孙子的左右旋情况,分四种情况进行判断。并利用connect34函数,做快速重构
*从而实现将失衡的g恢复正常平衡。并且rotateAt的返回值,就是重构后的对应子树的根节点的位置,把这个返回值
*作为原来g的父亲的孩子,即重新连接重构后的子树与原来的子树。
并且在connect34过程中,对树的高度也进行了更新。所以不必再度更新。*/
break;//一旦发生了重构,就不需要对后续的祖先进行高度更新和重构了。
}
else {
this->updateHeight(g);//未失衡,只需更新g的高度,不需要更新所有高度。
}
}
return x;
}
并且在插入算法中,我们用到了tallerChild函数,这个函数是获取当前节点的最高孩子节点。我们需要用这个函数,来获取传到rotateAt函数里面节点。
tallerChild函数~
顾名思义,获取当前节点的最高的孩子。
/*期望c++20可以支持,auto compareResult = stature(x->_lchild) <=> stature(x->_rchild);*/
template<typename T>
inline BinNode<T>*& AVL<T>::tallerChild(BinNode<T>*& x){
int lHeight = stature(x->_lchild);
int rHeight = stature(x->_rchild);
if (lHeight > rHeight) {
return x->_lchild;
}
else if (lHeight < rHeight) {
return x->_rchild;
}
else {//如果左右高度相等,则按父亲是左孩子还是右孩子,来返回对应的左右孩子
return (IsLChild(x) ? x->_lchild : x->_rchild);
}
}
效率分析~
该算法首先按照二叉搜索树的常规算法,在O(logn)时间内插入新节点x。
既然原树是平衡的,故至多检查O(logn)个节点即可确定失衡节点;如有必要,只需要进行一次重构,即可使局部乃至全树恢复平衡。
由此可见,AVL树的节点插入操作可以在O(logn)时间内完成。
(五)AVL的删除~
在删除新的节点的时候,有一个现象,就是无论如何:
1. 第一个失衡的节点,可能是父亲节点。
2. 每一次失衡,都必然只有一个祖先失衡,失衡的也只可能是祖先节点,不可能多个祖先同时失衡
3. 一个祖先修复平衡后,可能接下来导致下一个祖先也失衡,极端条件下,可能要重平衡多次。
删除代码~
AVL的删除的大体步骤跟BST的删除十分类似,只是删除之后,要进行修复平衡。因此需要借助在第三部分BST中定义的removeAt删除静态函数(建议理解了这个函数再来看AVL的删除)。
AVL删除的重平衡算法和插入的重平衡算法类似,只是对于祖先节点处理方式不一样,在插入算法中,一旦一个祖先恢复平衡,其他祖先就不需要进行平衡的检测了。而删除算法中,哪怕这个节点恢复了平衡,也要对其父亲进行平衡的检测。
只有当前节点是平衡的,并且高度没有发生变化时,重平衡才大功告成。
template<typename T>
bool AVL<T>::remove(const T& data)
{
BinNode<T>*& x = this->search(data);
if (!x)
return false;//如果不存在,返回false
removeAt(x, this->_hot);//删除此节点,并更新_hot值
this->_size--;//更新规模
for (BinNode<T>* g = this->_hot; g; g = g->_parent) {
if (!IsAvlBalanced(g)) {//如果失衡
BinNode<T>*& parent_child_Address = this->FromParentTo(g);//先记录之前的父亲节点的孩子指针
parent_child_Address = this->rotateAt(tallerChild(tallerChild(g)));//将孩子指针指向新的子树根节点
g = parent_child_Address;//更新g,g此时必然是处于平衡状态
}
else {
int gHeight = g->_height;
if (gHeight == this->updateHeight(g)) {//没失衡也要检查高度,祖先高度没变,则后序祖先也不需要进行更新。
break;
}
}
}
return true;//删除成功
}
效率分析~
较之插入操作,删除操作可能需在重平衡方面多花费一些时间。不过,既然需做重平衡的节点都是被删除节点的祖先,故重平衡过程累计只需不过O(logn)时间。
综合各方面的消耗,AVL树的节点删除操作总体的时间复杂度依然是O(logn)。
五、完整AVL.h~
#pragma once
#include "BST.h"
namespace mytree {
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
template<typename T>
BinNode<T>* AVL<T>::insert(const T& data){//无论data在不在子树中,返回值的_data均为data
BinNode<T>*& x = this->search(data);
if (x)
return x;
x = new BinNode<T>(data,this->_hot);//创建新节点
this->_size++;//更新规模
for (BinNode<T>* g = this->_hot; g; g = g->_parent) {
if (!IsAvlBalanced(g)) {//如果失衡
//必须要提前将地址取好,不然g的里面的数据发生变动,就会造成指针乱指。
BinNode<T>*& parent_child_Address = this->FromParentTo(g);
parent_child_Address= this->rotateAt(tallerChild(tallerChild(g)));
/*首先调用tallerChild函数,来确定到底旋转哪一个g的孙子节点(选择g的最高孩子的最高孩子作为旋转节点)
*在旋转过程中,对g的孩子,g的孙子的左右旋情况,分四种情况进行判断。并利用connect34函数,做快速重构
*从而实现将失衡的g恢复正常平衡。并且rotateAt的返回值,就是重构后的对应子树的根节点的位置,把这个返回值
*作为原来g的父亲的孩子,即重新连接重构后的子树与原来的子树。
并且在connect34过程中,对树的高度也进行了更新。所以不必再度更新。*/
break;//一旦发生了重构,就不需要对后续的祖先进行高度更新和重构了。
}
else {
this->updateHeight(g);//未失衡,只需更新g的高度,不需要更新所有高度。
}
}
return x;
}
template<typename T>
bool AVL<T>::remove(const T& data)
{
BinNode<T>*& x = this->search(data);
if (!x)
return false;//如果不存在,返回false
removeAt(x, this->_hot);//删除此节点,并更新_hot值
this->_size--;//更新规模
for (BinNode<T>* g = this->_hot; g; g = g->_parent) {
if (!IsAvlBalanced(g)) {//如果失衡
BinNode<T>*& parent_child_Address = this->FromParentTo(g);//先记录之前的父亲节点的孩子指针
parent_child_Address = this->rotateAt(tallerChild(tallerChild(g)));//将孩子指针指向新的子树根节点
g = parent_child_Address;//更新g,g此时必然是处于平衡状态
}
else {
int gHeight = g->_height;
if (gHeight == this->updateHeight(g)) {//没失衡也要检查高度,祖先高度没变,则后序祖先也不需要进行更新。
break;
}
}
}
return true;//删除成功
}
/*期望c++20可以支持,auto compareResult = stature(x->_lchild) <=> stature(x->_rchild);*/
template<typename T>
inline BinNode<T>*& AVL<T>::tallerChild(BinNode<T>*& x){
int lHeight = stature(x->_lchild);
int rHeight = stature(x->_rchild);
if (lHeight > rHeight) {
return x->_lchild;
}
else if (lHeight < rHeight) {
return x->_rchild;
}
else {//如果左右高度相等,则按父亲是左孩子还是右孩子,来返回对应的左右孩子
return (IsLChild(x) ? x->_lchild : x->_rchild);
}
}
}//namespace mytree
六、测试代码~
1.插入测试代码~
#include<iostream>
#include "AVL.h"
using namespace std;
using namespace mytree;
template<typename BinNodePtr>
void visitprint(BinNodePtr x) {
cout << x->_data;
}
int main() {
AVL<int>* avl = new AVL<int>;
for (int i = 0; i < 10; ++i) {
avl->insert(i);
}
avl->travLevel(visitprint<BinNode<int>*>);
cout << endl;
avl->travPre(visitprint<BinNode<int>*>);
cout << endl;
avl->travIn(visitprint<BinNode<int>*>);
cout << endl;
avl->travPost(visitprint<BinNode<int>*>);
cout << endl;
delete avl;
return 0;
}
3170258469
3102754689
0123456789
0214659873
2.插入测试图示~
3.删除测试代码~
#include<iostream>
#include "AVL.h"
using namespace std;
using namespace mytree;
template<typename BinNodePtr>
void visitprint(BinNodePtr x) {
cout << x->_data;
}
int main() {
AVL<int>* avl = new AVL<int>;
for (int i = 0; i < 10; ++i) {
avl->insert(i);
}
avl->travLevel(visitprint<BinNode<int>*>);
cout << endl;
avl->travPre(visitprint<BinNode<int>*>);
cout << endl;
avl->travIn(visitprint<BinNode<int>*>);
cout << endl;
avl->travPost(visitprint<BinNode<int>*>);
cout << endl<<endl;
for (int i = 0; i < 10; ++i) {
avl->remove(i);
avl->travIn(visitprint<BinNode<int>*>);
cout << endl;
}
delete avl;
return 0;
}
3170258469
3102754689
0123456789
0214659873
123456789
23456789
3456789
456789
56789
6789
789
89
9
4.删除测试图示~
七、后序文章链接~
- 基本二叉树节点,通用函数 二叉树节点
- 基本二叉树类的定义和实现 二叉树基类
- BST(二叉搜索树的实现) BST
- AVL(二叉平衡搜索树的实现)AVL
- B树的实现(如果你只想了解B树,可以跳过所有章节,直接看B树)B树
- 红黑树的实现 RedBlack
学一个东西,不知道其道理,不高明!