🎉个人名片:
🐼作者简介:一名乐于分享在学习道路上收获的大二在校生
🙈个人主页🎉:GOTXX
🐼个人WeChat:ILXOXVJE
🐼本文由GOTXX原创,首发CSDN🎉🎉🎉
🐵系列专栏:零基础学习C语言----- 数据结构的学习之路----C++的学习之路
🐓每日一句:如果没有特别幸运,那就请特别努力!🎉🎉🎉
一.AVL 树
1.1 AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。(上章提过)
所以就有人发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
AVL树的条件:
一棵AVL树可以是空树,是具有以下性质的二叉搜索树:
1.它的左右子树都是AVL树
2.左右子树高度之差(简称平衡因子 )的绝对值 不超过1 (-1/0/1)(如下图)(这里讨论的该节点的平衡因子=右子树高度-左子树高度)
注意 : 下图的平衡因子=左子树高度-右子树高度
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。
1.2 AVL树节点的定义
cpp
//树节点的定义
template<class K,class V>
struct AVLNode
{
AVLNode<K, V>* _left; //存储左节点
AVLNode<K, V>* _right; //存储右节点
AVLNode<K, V>* _parent; //存储父亲
pair<K,V> _kv; //存储数据
int _bl; //平衡因子
AVLNode(pair<K, V>& kv) //构造函数
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bl(0)
{}
};
1.3 AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
- 按照二叉搜索树的方式插入新节点
- 调整节点的平衡因子
平衡因子的处理方法:
Cur插入后,Parent的平衡因子一定需要调整,在插入之前,Parent 的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
- 如果Cur插入到Parent的左侧,只需给Parent的平衡因子-1即可
- 如果Cur插入到Parent的右侧,只需给Parent的平衡因子+1即可
此时:Parent的平衡因子可能有三种情况:0,正负1, 正负2
- 如果Parent的平衡因子为0,说明插入之前Parent的平衡因子为正负1,插入后被调整成0,此时满足 AVL树的性质,插入成功。
- 如果Parent的平衡因子为正负1,说明插入前Parent的平衡因子一定为0,插入后被更新成正负1,此时以Parent为根的树的高度增加,需要继续向上更新。
- 如果Parent的平衡因子为正负2,则Parent的平衡因子违反平衡树的性质,需要对其进行旋转处理。
【旋转代码在下面讲解后】
实现代码:
cpp
bool insert(pair<K,V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
}
//找插入位置
Node* parent = nullptr; //记录插入位置的父亲,方便插入后链接
Node* cur = _root;
while (cur)
{
if (cur->_kv > kv)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv < kv)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
//到这里就说明找到插入位置了,下面就开始插入了
//判断插在父亲的左边还是右边
if (cur == parent->left)
{
cur = new Node(kv);
parent->_left = cur;
cur->_parent = parent;
cur->_parent->_bl--; //如果是在左边插入,则--平衡因子
}
else if (cur == parent->right)
{
cur = new Node(kv);
parent->_right = cur;
cur->_parent = parent;
cur->_parent->_bl++; //如果是在右边插入,则++平衡因子
}
//调节上面节点的平衡值
while (parent)
{
//情况1:插入节点后父亲的平衡因子改变
if (parent->_bl == 1 || parent->_bl == -1)
{
Node* grandfather = parent->_parent;
if (parent = grandfather->_left)
{
grandfather->_bl--;
parent = grandfather;
}
else
{
grandfather->_bl++;
parent = grandfather;
}
}
//情况二:插入节点后父亲的平衡因子不改变
else if (parent->_bl == 0)
{
break;
}
//情况三:父亲的平衡因子已经不满足AVL树的条件
//需要旋转处理
else if ()
{
/下面的旋转代码后面讲解后贴出
//左单旋
if ()
{
}
//右单旋
else if ()
{
}
//左右双旋
else if ()
{
}
//右左双旋
else if ()
{
}
}
}
}
1.4AVL树的旋转
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
单旋分为:左单旋与右单旋
双旋分为:左右双旋与右左双旋
单旋思想
如图
如上图,上面的左右单旋,要么是在parent的左子树高 ,并且左子树中左边高 (左左高)
要么是parent的右子树高 ,并且右子树中右边高(右右高),这种及只需要旋转一边就可以解决不平衡的问题,哪边高,就往另一边旋转即可。
1.4.1左单旋
使用场景:
新节点插入较高右子树的右侧---右右:左单旋。
如图:长方形代表高度为h的子树。
具体例子:
解析:
含义解析:
cpp
pNodeR = parent->_right
pNodeRL = pNodeR->_left
思想解析:
如上图,右单旋是让pNodeRL节点成为parent的右孩子,然后parent自己变为pNodeR的左孩子,pNodeR变成这个子树的根。
平衡因子的调节:
单旋后pNodeR与parent的平衡因子都变为0;
注意:
在旋转过程中,有以下几种情况需要考虑:
- 50节点的左孩子可能存在,也可能不存在。
- 25可能是根节点,也可能是子树
如果是根节点,旋转完成后,要更新根节点。
如果是子树,可能是某个节点的左子树,也可能是右子树。
实现代码
cpp
//左单旋
void rotateL(Node* parent)
{
Node* pparent = parent->_parent; //记录所旋转根节点的父亲
Node* pNodeR = parent->_right;
Node* pNodeRL = pNodeR->_left;
if (pNodeRL) //如果该旋转节点的右节点的左孩子存在
parent->_right = pNodeRL;
pNodeR->_left = parent;
//新的父节点的链接
if (parent == _root) //若parent是根节点
{
_root = pNodeR;
pparent = nullptr;
}
else //parent不是根节点
{
if (pparent->_left == parent)
{
pparent->_left = pNodeR;
}
else
{
pparent->_right = pNodeR;
}
}
pNodeR->_bl = 0;
parent->_bl = 0;
}
1.4.2右单旋
使用场景:
新节点插入较高左子树的左侧---左左:右单旋
如图
具体例子:
解析:
含义解析
cpp
pNodeL = parent->_Left
pNodeLR = pNodeL->_right
思想解析
如上图,右单旋是让pNodeLR节点变为parent的左孩子,然后parent自己变为pNodeL的右孩子,pNodeL变成这个子树的根。
平衡因子的调节 :
单旋后pNodeL与parent的平衡因子都变为0;
在旋转过程中,有以下几种情况需要考虑:
- 5节点的右孩子可能存在,也可能不存在。
- 8可能是根节点,也可能是子树
如果是根节点,旋转完成后,要更新根节点。
如果是子树,可能是某个节点的左子树,也可能是右子树。
代码实现
cpp
/右单旋
void rotateR(Node* parent)
{
Node* pparent = parent->_parent;
Node* pNodeL = parent->_left;
Node* pNodeLR = pNodeL->_right;
if (pNodeLR)
parent->_left = pNodeLR;
pNodeL->_right = parent;
if (parent == _root)
{
_root = pNodeL;
pparent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = pNodeL;
}
else
{
pparent->_right = pNodeL;
}
}
pNodeL->_bl = 0;
parent->_bl = 0;
}
双旋思想
如图
如上图,如果parent的左子树高,并且左子树中的右子树高(左右高),或则是parent的右子树高,并且右子树的左子树高(右左高),则旋转一次不能解决问题,所以就有了双旋的思想。
1.4.3左右单双旋
使用场景:
新节点插入较高左子树的右侧---左右:先左单旋再右单旋
如图
具体例子:
解析:
因为是parent的左子树中的右子树高,所以只需要先将parent的左子树进行左旋,将parent的左子树变为左边高,则旋转后parent整个树就变为了左左高,再用上面单旋的思想,parent以旋转点进行右旋即可;
cpp
else if (parent->_bl == -2 && parent->_left->_bl == 1)
{
int bl = parent->_left->_right->_bl;
Node* pNodeL = parent->_left;
Node* pNodeLR = pNodeL->_right;
rotateL(pNodeL); //左旋转
rotateR(parent); //右旋转
if (-1 == bl) //分情况调节平衡因子
{
pNodeLR->_bl = 0;
pNodeL->_bl = 0;
parent->_bl = 1;
}
else if (1 == bl)
{
pNodeLR->_bl = 0;
parent->_bl = 0;
pNodeL->_bl = -1;
}
else
{
pNodeLR->_bl = 0;
parent->_bl = 0;
pNodeL->_bl = 0;
}
}
1.4.4右左单旋
使用场景:
新节点插入较高右子树的左侧---右左:先右单旋再左单旋
如图
具体例子:
解析:
因为是parent的右子树中的左子树高,所以只需要先将parent的右子树进行右旋,将parent的右子树变为右边高,则旋转后parent整个树就变为了右右高,再用上面单旋的思想,parent以旋转点进行左旋即可;
代码实现
cpp
else if (parent->_bl == 2 && parent->_right->_bl == -1)
{
Node* pNodeR = parent->_right;
Node* pNodeRL = pNodeR->_left;
int bl = pNodeRL->_bl;
rotateR(pNodeR); //先右旋
rotateL(parent); //再左旋
pNodeRL->_bl = 0; //分情况调节平衡因子
if (1 == bl)
{
parent->_bl = -1;
pNodeR->_bl = 0;
}
else if (-1 == bl)
{
parent->_bl = 0;
pNodeR->_bl = 1;
}
else
{
parent->_bl = 0;
pNodeR = 0;
}
}
双旋后平衡因子的调节
我们从结果来看,忽略过程,从图中可以得到
解析:
实际上就是将60的左孩子给了30的右孩子,把60的有孩子给了90的左孩子。
所以可以得出:
平衡因子的改变与60的平衡因子有关(与它的左右孩子有关)。
情况分为3种:60的平衡因子为(1,0,-1)
下图解为:右左双旋
当为1时:
结论:
pNodeRL的平衡因子为0
parent-->-1
pNodeR-->0
当为0时:
结论:
pNodeRL的平衡因子为0
parent-->0
pNodeR-->0
当为-1时:
结论:
pNodeRL的平衡因子为0
parent-->0
pNodeR-->1
左右双旋 的图解
旋转总结:
假如以Parent为根的子树不平衡,即Parent的平衡因子为2或者-2
分以下情况考虑:
- Parent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pNodeR 当pNodeR的平衡因子为1时,执行左单旋 当pNodeR的平衡因子为-1时,执行右左双旋
- Parent的平衡因子为-2,说明Parent的左子树高,设Parent的左子树的根为pNodeL 当pNodeL的平衡因子为-1是,执行右单旋 当pNodeL的平衡因子为1时,执行左右双旋
旋转完成后,原Parent为根的子树个高度降低,已经平衡,不需要再向上更新。
1.5 AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
- 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树 - 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确。
代码实现
方法一:
cpp
int Hight(Node* root) //计算该节点的高度
{
if (root == nullptr)
{
return 0;
}
int Hightleft = Hight(root->_left);
int Hightright = Hight(root->right);
return Hightleft > Hightright ? Hightleft + 1 : Hightright + 1; //返回左右子树高的那一个
}
bool _isbalance(Node* root)
{
if(root==nullptr)
{
return true;
}
int hightleft = Hight(root->_left); //计算左右子树的高度
int hightright = Hight(root->_right);
if (abs(hightright - hightleft) >= 2) //判断高度差
{
return flase;
}
if (hightright - hightleft !=root->_bl) //判断计算结果是否与该节点的平衡因子相等
{
cout << root->_kv->first<<':' << "异常" << endl;
return false;
}
return isbalance(root->_left) && isblance(root->_right); //递归
}
方法一有大量的重复计算(每一个节点都需要重新计算高度)
方法二更优
方法二:
cpp
bool _isbalance(Node* root,int& height) //height记录高度
{
if (root == nullptr)
{
height = 0;
return true;
}
if (!isbalance(root->_left,height) || !isblance(root->_right,height))
{
return false;
}
int heightleft = 0;
int heightright = 0;
if (abs(heightright - heightleft) >= 2) //如果高度差超过1,则不平衡,返回false
{
return false;
}
if (heightright - heightleft != root->_bl) //检查该节点的平衡因子是否正确
{
cout << root->_kv->first << ':' << "异常" << endl;
return false;
}
height = heightleft > heightright ? heightleft + 1 : heightright + 1; //计算height的值
return true;
}
bool isbalance()
{
return _isbalance(_root);
}
1.6 AVL树的性能分析
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)。
但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
本章完~