1.AVL树介绍
前面说过有二叉搜索树,并且也说到了二叉搜索树会出现极端情况,所有的新插入的节点都在同一边的可能,这种情况非常不利于我们使用。因此出现了AVL树,AVL树是最先发明的自平衡二叉搜索树。
AVL树需要遵循以下规则:首先它一定是一个二叉搜索树,它的左右子树也都是AVL树,并且左子树和右子树的高度差不能超过1。这样就可以保证我们在增删查改的时候的效率为log2(N)(以2为底N的对数)。
为了方便我们实现左右子树的高度差不超过1,我们引入了平衡因子的概念,平衡因子的数值等于右子树的高度减去左子树的高度。在两边子树高度差不超过1的前提下,那么平衡因子的大小只能取0,1,-1这三个数。
AVL树节点如下:
cpp
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent; //在计算平衡因子和后面的旋转需要用到父节点
pair<K, V> _kv; //第一个数据存储key,第二个数据存储value
int _bf; //平衡因子
AVLTreeNode(const pair<const K, V>& kv) //构造函数
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0) //新节点左右都为空,平衡因子为0
{}
};
2.AVL树插入时平衡因子的更新
AVL树的插入过程和二叉搜索树的插入是一样的,但是我们要改变平衡因子,并在平衡因子不是0,1,-1的时候做出改变。我们可以分析一下平衡因子可能的情况:
我们先看在左边插入的情况:

在左边插入有两种情况,一种是父节点没有右孩子,这个时候父节点的平衡因子从0->-1。还有一种情况则是父节点有右孩子,这个时候父节点的平衡因子就从1->0。
接下来是在右边插入的情况:

在右边插入有两种情况,一种是父节点没有左孩子,这个时候父节点的平衡因子从0->1。还有一种情况则是父节点有左孩子,这个时候父节点的平衡因子就从-1->0。
而在左插入和在右插入都有一种情况就是上面两张图左边的情况,这个时候以父节点为根的这棵树的高度发生了变化,都加了1。那么就会造成父节点的父节点的平衡因子改变,这种改变可能会一直往上走造成上面的平衡因子的改变。
那么什么时候应该停下平衡因子的改变呢?在父节点的平衡因子变成0的时候,平衡因子变成0只有两种情况,1->0或者-1->0。这里我们设父节点叫A节点,设A的父节点叫B节点,B节点的另一个孩子节点是C节点:

A节点从1变成0,那么说明A一开始插入之前A的右子树比左子树高出一,而插入之后变成0,说明左子树被插入了新节点,这个时候左右子树的高度都变成了右子树的高度,那么A整体的高度并没有变化,所以AC的高度差也没有发生变化,就不需要继续往上改变B的平衡因子。
A节点从-1变成0,那么说明A一开始插入之前A的左子树比右子树高出一,而插入之后变成0,说明右子树被插入了新节点,这个时候左右子树的高度都变成了左子树的高度,那么A整体的高度并没有变化,所以AC的高度差也没有发生变化,就不需要继续往上改变B的平衡因子。
我们可以根据上面的在推出A的平衡因子变成1或-1的时候需要继续向上改变平衡因子,这里作者就不再过多赘述了,大家可以自行推导。
而我们在插入时并不是只有上面四种情况,还有两种情况需要我们讨论,那就是造成极端情况的情况,平衡因子变成2或-2,如下情况:

这种情况下二叉树的平衡被打破,我们就需要旋转来让它保持原来的平衡,旋转的具体细节在后面标题3里面作者再详细说明。
根据上面所述,我们可以有以下代码:
cpp
#include<assert>
#include<iostream>
using namespace std;
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent; //在计算平衡因子和后面的旋转需要用到父节点
pair<K, V> _kv; //第一个数据存储key,第二个数据存储value
int _bf; //平衡因子
AVLTreeNode(const pair<const K, V>& kv) //构造函数
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0) //新节点左右都为空,平衡因子为0
{
}
};
template<class K, class V>
class AVLTree
{
typedef AVLTNode<K, V> node;
private:
node* _root = nullptr;
public:
bool insert(const pair<K, V>& kv)
{
if (_root == nullptr) //空树情况
{
node* root = new node(kv);
_root = root;
return true;
}
else
{ //非空树,先一直往下找要插入的位置
node* ptr = nullptr; //父节点(比较节点)
node* ptr1 = _root; //子节点
while (ptr1 != nullptr)
{
ptr = ptr1;
if (kv.first > ptr->_kv.first) //比父节点大,在右子树
{
ptr1 = ptr->_kv._right;
}
else if (kv.first < ptr->_kv.first) //比父节点小,在左子树
{
ptr1 = ptr->_kv._left;
}
else //跟父节点一样大,已经存在,不能插入重复元素
{
cout << "该节点已经存在,无法插入!" << endl;
return false;
}
}
node* newnode = new node(kv);
//找到了要插入的位置,插入并改变平衡因子
if (kv.first > ptr->_kv.first) //比父节点大,在右子树插入
{
ptr->_right = newnode;
}
else //比父节点小,在左子树插入
{
ptr->_left = newnode;
}
newnode->_parent = parent;
node* cur = newnode; //ptr是父节点 cur是孩子节点
//插入完成,统一进行平衡因子的改变
while (ptr != nullptr)
{
if (cur = ptr->_left) //插入是左边,不管ptr有没有右孩子,都必须平衡因子-1
{
ptr->_bf--;
}
else //插入是右边,不管ptr有没有左孩子,都必须平衡因子+1
{
ptr->_bf++;
}
//改变完父节点的平衡因子之后,再看看改变后的有没有超出范围
if (ptr->_bf == 0) //平衡因子为0,停止,不用再继续
{
break;
}
else if (ptr->_bf == -1 || ptr->_bf == 1) //平衡因子为-1或者1,继续往上
{
cur = ptr;
ptr = ptr->_parent;
}
else if (ptr->_bf == 2 || ptr->_bf == -2)
{
//.......
//.......
//.......
//按情况旋转
}
else //用于防止超出预期情况,用于发现可能自己未注意到的问题
{
arrest(0);
}
}
}
}
};
这里需要说明,最后的那个else其实可以不写,而倒数第二个的else if改成else,具体的平衡因子是2还是-2在后面再进行详细的完全的判断。加上最后的else是为了在测试代码多运行不同案例时帮助自己发现是否写的代码有所纰漏,如果有直接报错终止程序就说明少写了某种可能而不用一个一个看前面是哪里出了纰漏。
3.AVL树的旋转
前面说了,旋转是为了应对平衡因子出现2或者-2的情况,旋转一共有4种旋转,左旋,右旋,左右双旋,右左双旋。下面一步步进行分析:
我们统一设平衡因子崩溃的那个节点为A节点,左孩子为B节点,右孩子为C节点。

我们先看平衡因子变成2的情况,平衡因子变成2说明右子树比左子树高2,并且A节点原来的平衡因子为1,因为平衡因子每次必定是+/-1。A原来的平衡因子为1说明C子树比B子树高1,并且需要注意的是在这种情况下C的左右子树的高度一定是B子树的高度,如图:

DE子树的高度一定是等于B子树的高度的,这个解释起来可能有些晕,我会尽我可能的解释,看不懂的可以结合着图或者自己画一下多看几遍,实在看不懂的可以直接跳过默认是这样的就行。
首先,DE子树里面一定是至少有一个是比B子树高1的,这样才符合原来A的平衡因子是1的情况。我们假设D是h高度,E是h-1高度,然后开始新增节点,新增节点一定是在D上面,那样C的高度才会比B大2,A才能变成2的平衡因子,但是这个时候E就比D小2个高度,C的平衡因子就会变成-2。但是这是不可能出现的,因为我们处理平衡因子的改变是从下往上的,在发现A之前就一定会先发现C的平衡因子崩溃并且先处理完C的崩溃(这个时候C就是一个新的A),但是崩溃问题在旋转处理完成之后就不用继续往上,A就不可能平衡因子崩溃。所以这种情况不可能。同理还有E的高度是h-1,D的高度是h的情况。所以,A的平衡因子变成2只有DEB三者的高度相同的情况下才可能出现。这也是同理于A的平衡因子是-2的时候。
解释完之后我们再看新插入节点,有两种可能,往D或者往E上插入:

第一种往E上插入,此时C的平衡因子为1,那么这种情况下就需要左旋:C到AB之间,B做C的左子树,D做C的右子树,E做A的右子树,如下:

第二种往D上插入,此时C的平衡因子为-1,那么这种情况下就需要右左双旋:先把D提出来作为D节点与D的左子树和右子树,然后以D为中心右旋,以C为中心左旋,如下:

依次类推,当A的平衡因子为-2的时候,再根据B的平衡因子是1与-1分出了右单旋与左右双旋,这里作者就不进行推导,读者可以自行在纸上推导。
最后,就是代码部分:
cpp
#include<iostream>
using namespace std;
template<class K, class V>
class AVLTNode
{
public:
pair<K, V> _kv;
AVLTNode* _left;
AVLTNode* _right;
AVLTNode* _parent;
int _bf;
AVLTNode(pair<K, V> kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
AVLTNode(const K& key, const V& val)
:_kv(key, val)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
};
template<class K, class V>
class AVLTree
{
typedef AVLTNode<K, V> node;
private:
node* _root = nullptr;
public:
//还是仅在insert的时候申请资源,主要写单旋,双旋
void RotateR(node* parent) //右单旋
{
node* cur = parent->_left;
node* ppar = parent->_parent;
parent->_left = cur->_right;
if (parent->_left)
parent->_left->_parent = parent;
cur->_right = parent;
cur->_parent = ppar;
parent->_parent = cur;
if (ppar) //parent一开始不是根
{
if (parent == ppar->_left)
ppar->_left = cur;
else
ppar->_right = cur;
}
else
{
_root = cur;
}
cur->_bf = 0;
parent->_bf = 0;
}
void RotateL(node* parent) //左单旋
{
node* cur = parent->_right;
node* ppar = parent->_parent;
node* chl = cur->_left;
if (chl)
chl->_parent = parent;
parent->_right = chl;
cur->_left = parent;
parent->_parent = cur;
if (ppar) //parent一开始不是根
{
if (parent == ppar->_left)
ppar->_left = cur;
else
ppar->_right = cur;
}
else
{
_root = cur;
}
cur->_bf = 0;
parent->_bf = 0;
}
void RotateRL(node* parent) //右左双旋
{
node* cur = parent->_right;
node* childl = cur->_left;
int bf = childl->_bf;
RotateR(cur);
RotateL(parent);
if (bf == -1)
{
parent->_bf = 0;
childl->_bf = 0;
cur->_bf = 1;
}
else if (bf == 1)
{
parent->_bf = -1;
childl->_bf = 0;
cur->_bf = 1;
}
else
{
parent->_bf = 0;
childl->_bf = 0;
cur->_bf = 0;
}
}
void RotateLR(node* parent) //左右双旋
{
node* cur = parent->_left;
node* childr = cur->_right;
int bf = childr->_bf;
RotateL(cur);
RotateR(parent);
if (bf == -1)
{
parent->_bf = -1;
cur->_bf = 0;
childr->_bf = 0;
}
else if(bf == 1)
{
cur->_bf = -1;
parent->_bf = 0;
childr->_bf = 0;
}
else
{
parent->_bf = 0;
cur->_bf = 0;
childr->_bf = 0;
}
}
//插入
bool insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
node* cur = new node(kv);
_root = cur;
return true;
}
node* cur = _root;
node* parent = nullptr;
while (cur)
{
parent = cur;
if (kv.first > cur->_kv.first)
{
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
cur = cur->_left;
}
else
{
cout << "该值已经存在" << endl;
return false;
}
}
node* newnode = new node(kv);
if (kv.first > parent->_kv.first)
parent->_right = newnode;
else
parent->_left = newnode;
newnode->_parent = parent;
cur = newnode;
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 = cur->_parent;
}
else
{
if (cur == parent->_right)//包含左单旋,右左双旋
{
if (cur->_bf == 1 && parent->_bf == 2) //左单旋
{
RotateL(parent);
return true;
}
else if (cur->_bf == -1 && parent->_bf == 2)
{
RotateRL(parent);
return true;
}
}
else//包含右单旋,左右双旋
{
if (cur->_bf == -1 && parent->_bf == -2)
{
RotateR(parent);
return true;
}
else if (cur->_bf == 1 && parent->_bf == -2)
{
RotateLR(parent);
return true;
}
}
//if(parent == _root)
// break;
}
}
return true;
}
};
4.AVL树的验证
AVL树验证我们主要验证平衡因子是否符合条件,代码如下:
cpp
//前面的部分作者就不再赘述,大家可以跟前面的结合的看
public:
//判断是否是平衡二叉树
bool IsBalanceTree()
{
return _IsBalanceTree(_root);
}
private:
bool _IsBalanceTree(node* root)
{
// 空树也是AVL树
if (nullptr == root)
return true;
if (root->_bf < -1 || root->_bf > 1)
return false;
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
5.AVL树的删除与查找
AVL树的删除较为困难,建议自行查找学习,而查找部分的知识与前面文章二叉搜索树的内容相同,都是同样的原理,可以自行观看。