文章目录
AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度,原因在于调整后的二叉树是接近或等于完全二叉树的。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子,右子树高度-左子树高度)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 log 2 N \log_2^N log2N,搜索时间复杂度O( log 2 N \log_2^N log2N)。
注意:AVL树不一定需要平衡因子使用平衡因子是一种控制实现方式。
AVL树节点的定义
AVL树节点的定义:
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode(const std::pair<K,V>& value=std::pair<K,V>())
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
,_kv(value)
{}
AVLTreeNode<K,V>* _left;
AVLTreeNode<K,V>* _right;
AVLTreeNode<K,V>* _parent;
std::pair<K,V> _kv;
int _bf;//平衡因子
};
AVL树的接口
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K,V> Node;
private:
Node* _root;
public:
AVLTree()
:_root(nullptr)
{}
// 中序遍历
void InorderR()
{
_InorderR(_root);
}
bool insert(const std::pair<K, V>& kv)
{}
private:
void _InorderR(Node* root)
{}
//右旋转
void RotateR(Node* parent)
{}
// 左单旋
void RotateL(Node* parent)
{}
//左右双旋
void RotateLR(Node* parent)
{}
void RotateRL(Node* parent)
{}
};
AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
1、按照二叉搜索树的方式插入新节点
bool insert(const std::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->_left= cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
}
2、调整节点的平衡因子
正常来插入完结点后,就可以return了,但是我们要实现AVL树,我们需要检查是否满足平衡二叉树的规则,如果不满足规则我们需要对其进行调整,我们只要控制好AVL树的规则那么这棵树一定是一颗高效的搜索树。而AVL树是通过平衡因子调整来满足平衡规则。因此我们接下来需要分析当我们插入节点后,平衡因子的具体情况然后具体处理。
如何控制平衡因子?
1、 更新平衡因子---- 新增节点到根节点的祖先路劲
2、出现异样平衡因子,那么就需要旋转平衡处理
更新平衡因子最要有两种情况:
1、如果插入的是父亲左边那么父亲bf- -;
2、如果插入的是父亲右边那么父亲bf++;
更新完父亲先后,父亲的平衡因子分5种情况处理:
bf(平衡因子) | 分析 |
---|---|
bf==0 | 说明无需向上更新上面的祖先了,更新结束。说明更新之前是父亲为-1/1,现在把之前空位填上了,不会影响祖先的高度,祖先的子树高度不变祖先的平衡因子无需更新(如下样例图一所示) |
bf==1/-1 | 说明更新之前是父亲为0,子树有一边变高了,那么将会影响部分祖先,需要往上更新直到祖先为0(停止更新)或等于2/-2时进行旋转处理 ,最坏的情况下有可能一直更新到根结点(如样例图二) |
bf==2 /-2 | 说明 已经是不符合平衡规则了,需要旋转处理,达到平稳状态 |
样例图一:
样例图二:
注意:9为新插入结点
实现代码如下:
bool insert(const std::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->_left= cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
while (parent)//如果祖先为空那么无需继续操作了
{
if (parent->_left == cur)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
if (parent->_bf == 0)
{
break;//插入后已经达到平衡条件,无需继续操作
}
else if (parent->_bf == -1 || parent->_bf == 1)
{
// 继续往上更新
cur = parent;
parent = parent->_parent;
}
// 以parent为轴点,那边矮就旋转那边
else if (parent->_bf == -2 ||parent->_bf == 2)
{
//旋转处理
// 代码……
}
else // 走到这里说明之前就有问题了,断言处理
{
assert(false);
}
}
}
旋转处理
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。在此之前我们应该要知道旋转的目的以及规则。
1、旋转保持搜索树规则,控制平衡。
2、旋转的意义:这棵树平衡了,这棵树整体高度降了1。
根据节点插入位置的不同,AVL树的旋转分为四种:
注意:
我们只需要分析好4种旋转的情况,插入结点后平衡因子的情况和旋转后平衡因子的情况,就能对平衡因子正确的调整。
1. 新节点插入较高左子树的左侧—左左:右单旋
我们把上图称之为抽象图,只有抽象图能概括具像图的所有情况,原因在于具像图有无所种情况。矩形代表一颗子树,h代表子树的高度,上图总共有a,b,c抽象出来的子树。
往a子树插入新节点,子树根节点平衡因子变成-2,进行右单旋后,旋转之后这棵树平衡了,这棵树整体高度降了1。
旋转的过程,b子树成了60的左子树,60成了30的右子树,并且旋转之后并没有破坏搜索树的规则。
初始的子树高度为h+2,插入结点后的高度为h+3,旋转之后的高度为h+2,子树的高度保持初始值,因此我们不需要更新子树的祖先平衡因子。
在这个过程中其实就是把60压入到30的子树里。
右单旋代码实现
图例:
//右旋转
void RotateR(Node* parent)
{
Node* subL = parent->_left;//左结点
Node* subLR = subL->_right;//左节点的有结点
Node* pparent = parent->_parent;// 子树的父节点
//下面是处理好三叉连的关系
// 旋转完成之后,30的右孩子作为双亲的左孩子
parent->_left = subLR;
// 如果30的左孩子的右孩子存在,更新亲双亲
if(subLR)// 防止为空
subLR->_parent = parent;
// 60 作为 30的右孩子
subL->_right = parent;
// 更新60的双亲
parent->_parent = subL;
//如果成立,说明 parent==root
if (pparent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subL;
}
else
{
pparent->_right = subL;
}
subL->_parent = pparent;
}
// 根据调整后的结构更新部分节点的平衡因子
subL->_bf = 0;
parent->_bf = 0;
}
2. 新节点插入较高右子树的右侧—右右:左单旋
实现及情况考虑可参考右单旋。
即把30压入到60的子树里。
左单旋的代码实现
图例:
// 左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;//左结点
Node* subRL = subR->_left;//左节点的有结点
Node* pparent = parent->_parent;
parent->_right = subRL;
if (subRL)// 防止为空
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (pparent == nullptr)//parent==root
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subR;
}
else
{
pparent->_right = subR;
}
subR->_parent = pparent;
}
// 根据调整后的结构更新部分节点的平衡因子
subR->_bf = 0;
parent->_bf = 0;
}
双旋处理分析
在折现的情况下是不能使用单旋进行处理的,例如:
旋转之后并没有很好的解决。因为90是左子树高,30是右子树高,如果进行单旋,那么90会插入到30的右子树里,但是旋转后,又成了30是右边高,90是左边高。
我们发现如果要让单旋起效,我们应该要让两节点成直线转态。
例如:
因此,双旋其实就是要两次旋转,第一次旋转为了实现(两边成直线状态),第二次旋转使子树平衡。
3. 新节点插入较高左子树的右侧—左右:先左单旋再右单旋
第一步:在b子树里新增一个结点,更新平衡因子。
第二步:发现平衡因子需要双旋处理。
第三步:对30进行左单旋。
第四步:对60进行右单旋。
第五步:更新平衡因子,这里有三种情况,每种情况需要对应的去更新,因此们接下来分析三种情况。
为什么要分析三种情况?
原因在于,双旋操作会对60的子树进行瓜分,第一次旋转把b子树给了30,第二次旋转把c子树给了90。在这个基础上又因为,新增的结点可能在b/c子树里插入或者60就是新增的结点,因此双旋后的子树更新的平衡因子的值 是有三种情况的。
1、当subLR插入结点后平衡因子是-1时,说明插入的结点在subLR的左子树里,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。
2、当subLR插入结点后平衡因子是1时,说明插入的结点在subLR的右子树里,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。
3、当subLR插入结点后平衡因子是0时,说明subLR就是新增的结点,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。
代码实现
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
//3、更新平衡因子
if (bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if (bf == -1)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false); //在旋转前树的平衡因子就有问题
}
}
4. 新节点插入较高右子树的左侧—右左:先右单旋再左单旋
类似的。
代码实现
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
//1、以subR为轴进行右单旋
RotateR(subR);
//2、以parent为轴进行左单旋
RotateL(parent);
//3、更新平衡因子
if (bf == 1)
{
subRL->_bf = 0;
parent->_bf = -1;
subR->_bf = 0;
}
else if (bf == -1)
{
subRL->_bf = 0;
parent->_bf = 0;
subR->_bf = 1;
}
else if (bf == 0)
{
subRL->_bf = 0;
parent->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false); //在旋转前树的平衡因子就有问题
}
}
验证AVL树
bool IsBalance()
{
return _IsBalance(_root);
}
int Height(Node* root)
{
if (root == NULL)
return 0;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
bool _IsBalance(Node* root)
{
if (root == NULL)
return true;
// 对当前树进行检查
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf)
{
cout << root->_kv.first << "现在是:" << root->_bf << endl;
cout << root->_kv.first << "应该是:" << rightHeight - leftHeight << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
优化后的代码
//判断是否为AVL树
bool IsAVLTree()
{
int hight = 0; //输出型参数
return _IsBalanced(_root, hight);
}
//检测二叉树是否平衡
bool _IsBalanced(Node* root, int& hight)
{
if (root == nullptr) //空树是平衡二叉树
{
hight = 0; //空树的高度为0
return true;
}
//先判断左子树
int leftHight = 0;
if (_IsBalanced(root->_left, leftHight) == false)
return false;
//再判断右子树
int rightHight = 0;
if (_IsBalanced(root->_right, rightHight) == false)
return false;
//检查该结点的平衡因子
if (rightHight - leftHight != root->_bf)
{
cout << "平衡因子设置异常:" << root->_kv.first << endl;
}
//把左右子树的高度中的较大值+1作为当前树的高度返回给上一层
hight = max(leftHight, rightHight) + 1;
return abs(rightHight - leftHight) < 2; //平衡二叉树的条件
}
总结
主要是旋转处理,旋转处理有4中情况,每一种情况大致都一样,
第一步根据插入后子树根结点的平衡因子进行某种旋转选择。
第二步旋转之后又要根据各种情况来选择更新平衡因子。