本期我们来实现详细讲解AVL树
目录
AVL树的概念
二叉搜索树虽可以缩短查找的效率,但 如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下 。因此,两位俄罗斯的数学家 G.M.Adelson-Velskii和E.M.Landis 在 1962 年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过 1( 需要对树中的结点进行调整 ) ,即可降低树的高度,从而减少平均搜索长度。
一棵 AVL 树或者是空树,或者是具有以下性质的二叉搜索树:
它的左右子树都是 AVL 树 左右子树高度之差 ( 简称平衡因子 ) 的绝对值不超过 1(-1/0/1)
平衡因子 = 右子树的高度 - 左子树的高度
AVL树的增删查改最多走高度次,也就是O(logN)
代码实现
cpp
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K,V>* _left;
AVLTreeNode<K,V>* _right;
AVLTreeNode<K,V>* _parent;
int _bf;//平衡因子
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
,_bf(0)
{}
};
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//控制平衡
return true;
}
private:
Node* _root = nullptr;
};
我们先写出基础框架,然后我们来完成insert,此时我们完成的是平衡搜索树的,而AVL树还要控制平衡,另外需要注意的是,AVL树是三叉树,还有一个指针指向父亲节点
我们插入新节点后,平衡因子会发生变化,有多种情况,我们进行分析
1.新增节点在左,parent的平衡因子减一
2.新增节点在右,parent的平衡因子加一
3.更新后parent平衡因子 == 0,说明parent所在子树高度不变,不会再影响祖先,不用再继续沿着root的路径往上更新
4.更新后parent平衡因子 == 1 或者 -1,说明parent所在子树高度发生变化,会影响祖先,需要继续沿着root的路径往上更新
5.更新后parent平衡因子 == 2 或者 -2,说明parent所在子树高度发生变化,且不平衡,需要对parent所在子树进行旋转,让他重新平衡
6.更新到根节点
在上面的条件中,如果是3和5,那么就结束插入,如果是4,那么继续循环,更新祖先的平衡因子
6也是插入结束,这是最坏的情况,比如满二叉树插入新节点,他的平衡因子就会一直更新到根节点
cpp
//控制平衡
while (parent)
{
if (cur == parent->_left)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
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)
{
//旋转
}
else
{
return false;
}
}
控制平衡代码也非常长,这里是我们对于上面的逻辑进行的实现,最后为什么还要有一个else呢?这是当我们的代码出现错误时,如果错误在这里,说明平衡因子在插入前就有问题,方便我们判断,当然,上面的逻辑还差一个旋转,下面我们来看看到底怎么进行旋转
我们在旋转时要注意两个问题,一个是旋转后要保证它还是搜索树,二是要变为平衡树且降低这个子树的高度
假设我们有这样两棵树,其中红色的节点是我们新插入的节点,此时的平衡因子就出现了问题,需要我们进行旋转
我们对第一棵树这样旋转,这是什么操作呢?3作为1的左子树,下面的节点一定是比1大的,所以我们把3的左子树作为1的右子树(这里3的左子树是空,所以1的右子树也是空),然后把1作为3的左子树,这样就可以把1按下来,使整棵树的高度降低
同样的,我们看第二棵树,我们一样的把3的左子树作为1的右子树,也就是2变为1的右子树 ,然后把1变为3的左子树,这样整棵树的高度就降下来了,平衡因子问题也就解决了
当然这还没完,当我们旋转完后,还要把父亲节点给更正
上面的只是一种情况,我们的parent,也就是平衡因子为2的节点都是根节点,如果不是根节点,我们是需要提前保存一下,然后更正父亲节点的位置
cpp
void RotateL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
parent->_right = curleft;
if (curleft)
{
curleft->_parent = parent;
}
cur->_left = parent;
Node* ppnode = parent->_parent;
parent->_parent = cur;
if (parent == _root)//parent是根节点
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
parent->_bf = cur->_bf = 0;
}
整个过程是非常复杂的,大家需要画图来进行辅助理解 ,我们给这个函数传的参数是平衡因子为2的节点,也就是parent
我们的旋转其实还没有完成,因为旋转的情况不可能只有这么简单的两种,我们把上面的这副图叫做抽象图,它没有画出具体的图,但是这里是一个代表,画了abc3棵子树,插入之前,abc是3棵AVL子树,如果在c里插入一个新节点,导致c的高度变为h+1,就会导致60的平衡因子变为1,进而导致30的平衡因子变为2,这棵树就不平衡了,就需要旋转
下面我们来分情况进行讨论
第一种情况是h=0,大概是这个样子
第二种情况是h=1,新增节点是60的右孩子的孩子,无论是左右孩子都会导致30的平衡样子变为2,而需要旋转,这里是有两种情况的
第三种情况是h=2,此时情况就非常复杂了 ,我们可以确定ab是xyz的任意一种,而c是z,只有这样在c新增节点才能让这棵树进行旋转,再加上ab的选择,这棵树插入前的可能形状组合有 3*3*1 = 9种,而在c插入新节点有4种情况,所以这里一共是有 4*9 = 36种情况的,大家想想,这还只是h为2的情况,如果h是3,4,5呢?所以是这里的情况是无穷无尽的,于是就需要有抽象图来帮助我们进行理解
但是不管它是什么情况,我们只需把60的左边作为30的右边,再把30作为60的左边即可,以不变应万变
上面我们实现了左单旋,下面我们就需要实现右单旋了
结合这副图,我们可以理解,右单旋是将30的右边给60的左边,再把60作为30的右边,这里和左单旋是类似的,下面我们继续分情况讨论
我们看h=0和h=1的情况,h=0只有一种情况,而h=1是有两种情况的,在30左孩子的左右新增节点都会发生旋转
h=2 也与左单旋基本一样,只不过此时a是z,bc是xyz中的一种,这里同样一共有36种情况
下面我们来写代码
cpp
void RotateR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
parent->_left = curright;
if (curright)
{
curright->_parent = parent;
}
Node* ppnode = parent->_parent;
cur->_right = parent;
parent->_parent = cur;
if (ppnode == nullptr)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
parent->_bf = cur->_bf = 0;
}
有了左旋的经验,我们可以轻松写出右旋
实现了单旋,下面我们来看看双旋
这种情况我们该如何进行旋转呢?这里就需要进行双旋,我们可以看出,如果插入新节点后,路径长的像直线就是单旋,是折线就是双旋
首先我们来看h=0的情况,这里60就是新增的节点
再看h=1,在60的左边或者右边新增都会导致双旋
我们再看h=2的情况,首先a和d是xyz中的任意一种, 而60的左右子树高度为h-1,也就是1,所以60的左右孩子就是一个节点,此时新增节点有4种情况,在60的两个孩子的左右子树4个位置
这里有共用 3*3*4 = 36种情况,h=3,4,5的情况就更加复杂,这里也是无穷无尽的
接着我们来看双旋的操作,我们先以90为旋转点,进行右单旋,再以30为旋转点,进行左单旋
我们先以90为旋转点进行右单旋,也就是90为parent,我们把60的右边作为90的左边,再把90作为60的右边,然后我们就可以发现,这棵树从折线变成了直线
再以30作为旋转点,也就是parent,我们就可以得到一棵正常的树
我们再看这棵树,同样的,我们先以90为旋转点,进行右单旋
再以30为旋转点,进行左单旋,就完成了
我们分析可以得到,双旋结果的本质是:60的左边给30的右边,60的右边给90的左边,60成了这棵树的根
根据插入节点位置的不同,30和90的平衡因子也是不同的
而当60自己就是新增节点时,又是一种情况
我们来看,当h>0时,我们在c插入新节点诱发旋转,c从h-1变为h,最终30的平衡因子是-1 ,c最后变成了90的左边
如果是在b插入新节点诱发旋转,b最终变为30的右边,90的平衡因子变为1
上面是两种情况,还有一种就是60本身是新增,60的平衡因子就是0,所以这里我们一共分出3种情况
也就是说,这里最终的平衡因子是需要看60的,30是parent,90是cur,60是curleft,也就是说,我们要看curleft的平衡因子
cpp
void RotateRL(Node* parent)//右左双旋
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = -1;
}
else if(bf == -1)
{
cur->_bf = 1;
curleft->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
我们根据curleft的平衡因子来确定最终的平衡因子
既然有右左双旋,那么就肯定还有左右双旋
这里的代码其实是基本一样的,我这里就不详细说了,下面我们直接来实现代码
cpp
void RotateLR(Node* parent)//左右双旋
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0)
{
cur->_bf = 0;
curright->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
cur->_bf = -1;
curright->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
cur->_bf = 0;
curright->_bf = 0;
parent->_bf = 1;
}
else
{
assert(false);
}
}
我们根据curright的平衡因子来确定最终的平衡因子
这些旋转完成后,我们需要把insert补全
cpp
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//控制平衡
//更新平衡因子
while (parent)
{
if (cur == parent->_left)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
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)
{
RotateL(parent);
}
else if(parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
else if(parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
break;
}
else
{
assert(false);
}
}
return true;
}
可以看到,我们是分了非常多的情况进行讨论的
下面我们再写一个函数来判断我们的AVL树是否正常
cpp
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)//判断是否为AVL树
{
if (root == nullptr)
{
return true;
}
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf)//平衡因子不正常时
{
cout << "平衡因子异常:" <<root->_kv.first<< "->" << root->_bf << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& IsBalance(root->_left) //所有的子树都要符合
&& IsBalance(root->_right);
}
这里有两个IsBalance,空参数的那个是为了在外边使用,因为_root是私有的,下面的那个可以写成子函数,不写也没事,因为构成了函数重载
接着我们测试一下,没有问题
再用1w个随机值测试一下(有些同学写的代码跑不过某些随机值),这里也没有问题
我们上面实现了插入,删除我这里就不写了,给大家提供一下思路,感兴趣的可以自己去实现一下
删除也是分为三部,按搜索树查找节点删除,更新平衡因子,出现异常时旋转
我们看这张图片,我们删除了2,此时1的平衡因子变成了-1,但是此时是不需要更新的,因为高度没有发生变化
而如果我们把9删掉,8的平衡因子变为了0 ,此时就需要更新了,因为高度是发生了变化
大家想想,插入时我们是填充低的树,高度才会变化,而删除是删掉高的树,高度会变化,原来的平衡因子是-1或者1,就是一边高一边低,然后变成了0,说明高的那一端被删掉了,所以就需要更新
所以我们一路往上更新,这里到根节点结束了
我们继续删除,这次我们把6和8删掉,此时根节点的平衡因子变成了-2
然后我们进行旋转,这里是右单旋,旋转其实没什么区别,就是那几种,但是平衡因子的更新会变得非常麻烦,旋转我们也是根据删除的路径,而不是插入时的路径,是非常麻烦的
具体的大家可以在网上查一查,或者根据数据结构相关的书籍来看一看
全部代码
cpp
using namespace std;
#include<iostream>
#include<assert.h>
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K,V>* _left;
AVLTreeNode<K,V>* _right;
AVLTreeNode<K,V>* _parent;
int _bf;//平衡因子
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
,_bf(0)
{}
};
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//控制平衡
//更新平衡因子
while (parent)
{
if (cur == parent->_left)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
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)
{
RotateL(parent);
}
else if(parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
else if(parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
break;
}
else
{
assert(false);
}
}
return true;
}
void RotateL(Node* parent)//左单旋
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
parent->_right = curleft;
if (curleft)
{
curleft->_parent = parent;
}
cur->_left = parent;
Node* ppnode = parent->_parent;
parent->_parent = cur;
if (parent == _root)//parent是根节点
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
parent->_bf = cur->_bf = 0;
}
void RotateR(Node* parent)//右单旋
{
Node* cur = parent->_left;
Node* curright = cur->_right;
parent->_left = curright;
if (curright)
{
curright->_parent = parent;
}
Node* ppnode = parent->_parent;
cur->_right = parent;
parent->_parent = cur;
if (ppnode == nullptr)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
parent->_bf = cur->_bf = 0;
}
void RotateRL(Node* parent)//右左双旋
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
int bf = curleft->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0)
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
cur->_bf = 0;
curleft->_bf = 0;
parent->_bf = -1;
}
else if(bf == -1)
{
cur->_bf = 1;
curleft->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
void RotateLR(Node* parent)//左右双旋
{
Node* cur = parent->_left;
Node* curright = cur->_right;
int bf = curright->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0)
{
cur->_bf = 0;
curright->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
cur->_bf = -1;
curright->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
cur->_bf = 0;
curright->_bf = 0;
parent->_bf = 1;
}
else
{
assert(false);
}
}
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)//判断是否为AVL树
{
if (root == nullptr)
{
return true;
}
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf)//平衡因子不正常时
{
cout << "平衡因子异常:" <<root->_kv.first<< "->" << root->_bf << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& IsBalance(root->_left) //所有的子树都要符合
&& IsBalance(root->_right);
}
private:
Node* _root = nullptr;
};
以上即为本期全部内容,希望大家可以有所收获
如有错误,还请指正