目录
之前我们学习了二叉搜索树,注意到二叉搜索树在最坏情况下会退化成单支树,查找效率会大幅度下降,而AVL树解决了这个问题,它保证整棵树的查找效率始终为O(logN)
1.AVL树的概念
AVL树是一颗平衡二叉树,从名字可以看出,AVL树的左右子树的高度肯定不会相差很大,不然就不能称之为平衡,所以引出AVL树的性质,对于一个节点来说,它的左右子树高度差不会超过1,且它的左右子树均继承这个性质,从而保证了整棵树左右相对平衡,这里我们引入一个平衡因子的概念,每个节点都包含一个平衡因子,平衡因子的值等于右子树高度减去左子树高度,且取值范围为-1,0,1这三个值,不过AVL树不一定需要平衡因子,只不过有了平衡因子控制和观察树是否平衡会变得更简便
那么为什么高度差不控制为0而是不超过1,假如现在有一棵满二叉树,它每一层的节点个数都满了,如果此时再插入一个节点,那么对于根节点左右子树的高度差一定会发生变化,所以高度差是不可能严格控制到0的,但是保持在0或者1是可以做到的
2.AVL树的结构
pair的作用就是将一组值耦合在一起,形成一个组合,没有其他特殊含义,将key和value作为一份数据存储在节点中,使用更加严谨和方便
之前学过的二叉搜索树中,每个节点只有key,这里只是多了一个value作为它实际存储的值,依旧是使用key来进行比较遍历,确定每个节点插入的位置,只需把key当作重点即可
而对于AVL树的每个节点除了左右节点,还新增了父亲节点,这样就构成了双向的操作,对于一个节点,可以直接查找到它的父亲节点,在迭代器进行遍历的时候就会很方便,AVL树的迭代器部分后面在讲
cpp
//AVL树的每个节点,定义为一个结构体
template<class K,class V>
struct AVLTreeNode {
pair<K, V> _kv;//将一对值耦合在一起,分别是key和value
//定义三个节点,对应左右节点和父亲节点
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf = 0;//平衡因子,范围是0~2
//初始化列表,将本节点和平衡因子进行初始化
//其余节点置为nullptr,后续进行赋值
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
};
3.AVL树的插入
首先先按照二叉搜索树的规则进行插入,然后就要通过平衡因子进行判断是否出现不平衡的情况,并且新插入节点后,对于它的部分祖先节点的平衡因子会产生影响,要向上进行更新
例如在插入X节点之前,A,B,C的平衡因子均为0,但是在C右侧插入节点后,C的平衡因子变为1,B平衡因子变为1,C的平衡因子变为-1,这种情况就是对所有祖先节点的平衡因子都产生了影响,但是如果原来在最左侧还有一个节点,那么插入X节点时就不会对A的平衡因子产生影响

但是此时,如果在X的左右节点插入一个新的节点,此时就会出现不平衡,因为C的平衡因子会变成2,破坏了AVL树左右子树不高度差不超过1的规则,所以要使用旋转的操作进行调整,在旋转之后当前子树会变得平衡,高度和插入前保持一致,不会影响祖先节点,不用继续向上更新,对于旋转的具体操作后续会讲,这里先知道平衡因子异常需要进行旋转
3.1平衡因子的更新
更新平衡因子有几条原则
1.平衡因子=右子树高度-左子树高度
2.只有子树高度变化才会影响当前节点的平衡因子
3.对于某节点来说,在他的左右插入新的节点,如果插入在右子树那么平衡因子++,插入在左子树平衡因子--,注意新插入的节点和某节点是直接连接的,也就是父子节点才能这样更新
4.某节点所在子树高度是否发生变化,决定是否需要继续向上更新平衡因子
更新结束的条件:
我这里将某子树的根节点叫做node,方便讲解
注意这里更新后,node节点的平衡因子是发生变化的,如果和原来一样就没有讨论意义了
1.对于node节点更新后平衡因子为0,且更新过程中它的平衡因子是从1或者-1变化到0的,说明它的左右子树原来一边高,更新后左右子树平衡了,但是更新后node所在子树的整体高度并不会变化,例如原来左子树高,那么在右子树新插入一个节点使得右子树高度和左子树保持一致,整体高度依旧是左子树的高度,所以此时更新结束,不用向上更新
如图,节点A在插入红色节点前,平衡因子是1,插入节点后使得A的平衡因子为0,此时该子树高度未发生变化,不需要调整

2.更新后node节点的平衡因子为-1或者1,且原来的平衡因子为0,说明更新前node左右子树平衡,更新后node节点的左右子树一边高,node所在子树的平衡因子符合要求,但是高度发生了变化,需要继续向上调整
如图,对于A节点来说原本平衡因子为0,更新后左子树高,平衡因子变为-1,此时需要继续向上调整,对于节点B来说左子树变高,平衡因子从0变化到-1,所以最坏情况可能需要调整到根节点

3.更新后node节点平衡因子为-2或者2,且原来平衡因子为-1或者1,说明更新前左右子树有一边高,且新插入的节点插入在高的那一边,此时需要进行旋转的操作,将node所在子树旋转,使得该子树的高度和插入前保持一致,所以旋转后子树高度不变,不需要继续向上调整
如左图,此时插入节点后,使得A的平衡因子变为-2,需要进行旋转的操作,最后这棵树会变成右边这样,具体怎么执行旋转的操作后文单独讲解


4.插入代码的实现
ps:旋转代码在讲解完旋转操作后给出
那么按照刚才讲的插入逻辑,先查找对应位置,然后插入节点,之后调整平衡因子,判断是否合理,如果平衡因子异常则需要调整
cpp
//插入节点
bool Insert(const pair<K, V>& kv) {
if (_root == nullptr) {
//如果这颗树还没节点,进行根节点的插入
_root = new Node(kv);
return true;
}
//树有节点,那么进行查找,确定插入的位置
Node* parent = nullptr;
Node* cur = _root;
//从根开始遍历,插入值大于节点往右走,小于往左走
while (cur) {
if (cur->_kv.first > kv.first) {
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first) {
parent = cur;
cur = cur->_right;
}
else return false;
}
//找到位置后,将cur新建节点,叶子节点平衡因子为0
cur = new Node(kv);
cur->_bf = 0;
//确实是父亲节点的左边还是右边
if (cur->_kv.first < kv.first) {
parent->_left = cur;
}
else {
parent->_right = cur;
}
cur->_parent = parent;
//开始计算平衡因子,如果超出[-1,1]就要进行旋转
while (parent) {
if (cur == parent->_right) {
parent->_bf++;
}
else {
parent->_bf--;
}
//如果该节点原来是-1或者1,说明左右一边高
//变成0之后平衡,该子树高度不变,不对祖先节点产生影响
//直接跳出
if (parent->_bf == 0) {
break;
}
else if (parent->_bf == 1 || parent->_bf == -1) {
//说明原来平衡,现在不平衡
//会对祖先节点产生影响,要向上调整
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2) {
//此时需要进行旋转的操作,让该子树保持平衡
//有四种旋转操作
//右旋,左旋,左右旋,右左旋
//当该子树,左边高右边低
//此时进行右旋
if (parent->_bf == -2 && cur->_bf == -1) {
RotateR(parent);
//右旋
}
else if (parent->_bf == 2 && cur->_bf == 1) {
RotateL(parent);
//左旋,和右旋情况相反
}
else if (parent->_bf == -2 && cur->_bf == -1) {
RotateLR(parent);
//左右旋
}
else if (parent->_bf == -2 && cur->_bf == -1) {
RotateRL(parent);
//右左旋,和左右旋情况相反
}
else return false;
}
}
return true;
}
5.旋转
旋转分为四种,左旋,右旋,左右旋,右左旋
5.1.右旋
如图情况,对于节点A来说,在B的左子树插入一个新节点后,A的左子树高度为h+2,右子树高度为h,所以需要进行旋转的操作,既然是右旋,那就是向右进行旋转
我们的目的是旋转之后高度和插入前保持一致,所以此时A节点不再是这棵子树的根节点,我们选择让B节点成为新的根节点,然后A成为B的右节点,但是这样B原来的右子树就没地方连接了,注意到A成为B的右节点前,要先断开左子树的连接,所以我们可以把B的右节点给到A的左节点

所以根据以上逻辑,让A的左节点B成为新的根节点,A成为B的右节点,然后将B的右子树作为A的左子树,以下是旋转之后的图,此时我们来验证一下是否符合搜索二叉树的规则,对于B的右子树来说,B是A的左子树,所以B的右子树一定小于A,放在A的左子树位置没问题,因为B比A小,所以A作为B的右子树也没问题,所以旋转不会破坏搜索二叉树的规则,后续就不再赘述

5.2左旋
左旋的操作就是和右旋相反
如图,新插入的节点在右侧,此时节点A的平衡因子为2,那么需要进行左旋的操作

逻辑就是和右旋相反,我们将B作为新的根节点,然后将B的左子树作为A的右子树,将A连接到B的左节点即可,以下为旋转之后的结果

还有一个注意事项,那就是这棵树可能也只是某个节点的子树,所以我们在旋转之前,要先记录这棵树的父亲节点,然后旋转完毕后,让父亲节点与新的根节点进行连接,后面的双旋也是一样
5.3左右旋
如图,对于A节点是左边子树高,对于B节点是右边子树高,那么如果只用一次上面学的单旋,无法达到目的(可以自己画图验证一下),这时候就需要进行两次旋转且方向相反
我们以B节点作为根节点,先进行一次左旋,然后此时整棵树只有左边子树高,那么再以A节点进行一次右旋,此时整棵树就达到了平衡

以下是旋转之后的图,可以简化逻辑,我们将C作为新的根节点,B和A作为C的左右节点,然后将C的左右子树分别给到B和A即可,当插入节点在C左侧时,该节点会分到B的右子树,当插入节点在C右侧时,该节点就会分到A的左子树,也就是图中红色h和h-1这两个位置

5.4右左旋
情况就是和左右旋相反,先进行一次右旋,再进行一次左旋、
以下是旋转之后的图

6.旋转的代码实现
对于右旋来说,我们需要找到当前节点parent,当前节点的左节点subL,subL的右节点subLR,以及当前节点的父节点pparent,因为这四个节点的连接发生了改变
左旋逻辑相反,按照这个思路反着找四个节点即可
cpp
//右旋
void RotateR(Node* parent) {
//找到当前节点的左节点,和左节点的右节点
//这三个节点之间要进行变化
Node* subL = parent->_left;
Node* subLR = subL->_right;
//如果左节点的右节点存在,需要调整其父节点
parent->_left = subLR;
if (subLR)subLR->_parent = parent;
//找到该子树上一层的节点,旋转可能只是在局部发生
Node* pparent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
//如果该子树就是全部的树,那么子树的根节点作为整棵树的根节点
if (parent == _root) {
_root = subL;
subL->_parent = nullptr;
}
//否则判断原来子树的根节点是上一层父亲节点的做还是右,进行链接
else {
if (parent = pparent->_left) {
pparent->_left = subL;
}
else {
pparent->_right = subL;
}
subL->_parent = pparent;
}
//旋转后应该是平衡的,平衡因子为0
parent->_bf = subL->_bf = 0;
}
//左旋
void RotateL(Node* parent) {
//找到当前节点的左节点,和左节点的右节点
//这三个节点之间要进行变化
Node* subR = parent->_right;
Node* subRL = subR->_left;
//如果右节点的左节点存在,需要调整其父节点
parent->_right = subRL;
if (subRL)subRL->_parent = parent;
//找到该子树上一层的节点,旋转可能只是在局部发生
Node* pparent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
//如果该子树就是全部的树,那么子树的根节点作为整棵树的根节点
if (parent == _root) {
_root = subR;
subR->_parent = nullptr;
}
//否则判断原来子树的根节点是上一层父亲节点的做还是右,进行链接
else {
if (parent = pparent->_left) {
pparent->_left = subR;
}
else {
pparent->_right = subR;
}
subR->_parent = pparent;
}
//旋转后应该是平衡的,平衡因子为0
parent->_bf = subR->_bf = 0;
}
左右双旋,找到需要旋转的子树,找到对应节点复用左旋和右旋即可
因为刚才讲过,(按照旋转部分的图例)双旋转实际上对节点C的左右节点进行了分配,所以我们要判断新插入的节点在C节点的左侧还是右侧,这决定了AB节点的平衡因子
cpp
//左右旋转代码
void RotateLR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
//本质是将subLR的左或者右节点分配给subL或者parent
int bf = subLR->_bf;
//对两个节点进行左旋右旋
RotateL(subL);
RotateR(parent);
if (bf == 0) {
//此时只有三个节点,旋转完成后全部平衡
subL->_bf = 0;
parent->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1) {
//subLR右边的节点会给到parent的左边
//所以parent平衡,subL左边高
subL->_bf = -1;
parent->_bf = 0;
subLR->_bf = 0;
}
else if (bf == -1) {
//subLR左边的节点会给到subL的右边
//所以subL平衡,parent右边高
subL->_bf = 0;
parent->_bf =1;
subLR->_bf = 0;
}
}
//右左双旋
void RotateRL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
//本质是将subLR的左或者右节点分配给subL或者parent
int bf = subRL->_bf;
//对两个节点进行左旋右旋
RotateL(subR);
RotateR(parent);
if (bf == 0) {
//此时只有三个节点,旋转完成后全部平衡
subR->_bf = 0;
parent->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 1) {
//subLRL右边的节点会给到parent的左边
//所以subR平衡,parent左边高
subR->_bf = 0;
parent->_bf = -1;
subRL->_bf = 0;
}
else if (bf == -1) {
//subLR左边的节点会给到parnet的右边
//所以parent平衡,subR右边高
subR->_bf = 1;
parent->_bf = 0;
subRL->_bf = 0;
}
}
7.AVL树的平衡检验
我们只需要对每个节点都检验,它的左右子树高度差是否超过1即可,所以先写一个计算二叉树高度的函数,然后递归整棵树,复用计算左右子树高度的代码,令它们相减即可
cpp
int Height(Node* root) {
if (root == nullptr) {
return 0;
}
return (Height(root->_left) > Height(root->_right) ? Height(root->_left) : Height(root->_right)) + 1;
}
bool _IsBanlance(Node* root) {
if (root == nullptr) {
return true;
}
int LHeight = Height(root->_left);
int RHeight = Height(root->_right);
int bf = RHeight - LHeight;
if (abs(bf) > 2 || bf != root->_bf) {
cout << "平衡因子异常" << endl;
return false;
}
return _IsBanlance(root->_left) && _IsBanlance(root->_right);
}