二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均 搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1.它的左右子树都是avl树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
注:如果一棵二叉树是平衡二叉树,它就是AVL树,如果有N个结点,O(log_2 n),搜索时间复杂度O(log_2 n)。
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K,V>* _left;//三叉链
AVLTreeNode<K,V>* _right;
AVLTreeNode<K,V>* _parent;
pair<K,V> _kv; //存储的键值对
int _bf;//balance factor平衡因子
AVLTreeNode(const pair<K, V>& kv)构造函数
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_bf(0)
{
}
};
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
1.按照二叉搜索树的方式插入新节点
2.调整结点的平衡因子
3.如果出现不平衡,就旋转调整
对于二叉搜索树的插入规则:
1.插入结点key比当前节点小就插入左子树
2.插入结点key比当前节点大就插入右子树
3.插入节点key和当前节点相等就插入失败
对于平衡因子的变化:
一个节点的平衡因子是否需要更新,取决于该节点的左右子树的高度是否发生了变化,因此插入一个结点后,该节点的祖先的平衡因子也可能发生变化。
所以我们要倒序更新,更新规则:
1.如果新增结点在parent的左边,parent的平衡因子++。
2.如果新增结点在parent的右边,parent的平衡因子–。
每次更新完一个平衡因子,都需要继续向上更新 判断:
1.如果parent的平衡因子变为-1 或 1,则需要往上继续更新平衡因子。
解析:就像上面那个图一样,parent变为 1 或者 -1说明它已经不平衡了, parent的parent,因此需要继续向上调整。
2.如果parent平衡因子变为了0,则不需要继续更新
解析:说明新节点插入后,新节点的parent的左右子树平衡了,并不会影响parent的parent,所以不需要继续向上更新
3.如果parent的平衡因子变为了 2 或 -2 ,表明parent结点为 跟结点的子树需要进行旋转调整。
解析:此时已经很不平衡了,正常的调整无法平衡,需要旋转调整
对于旋转调整:
我们将插入节点称为cur,它的父节点为parent,当parent的平衡因子变为2/-2 的时候,cur的平衡因子必然为1/-1 ,不可能是0;
解释:假如cur的平衡因子为0,cur已经平衡了, 这时候无论cur在parent的左子树还是右子树,另一个子树无论是否为空,都不能使parent的平衡因子变为2/-2,所以上面成立。
根据这个结论,我们可以将旋转处理分为以下四类:
1.parent的平衡因子为2,cur为1,左单旋转
2.parent的平衡因子为-2,cur为-1,右单旋转
3.parent的平衡因子为-2,cur为1,左右双旋转
4.parent的平衡因子为2,cur为-1,右左双旋转
旋转后就不需要再向上更新平衡因子了。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/6cd326636fd44025b210223be458d970.png
左单旋步骤:
左单旋并不会破环原本二叉搜索树的性质
b比60大 ,到30的右子树满足比30大。
30比60小 ,到60的左子树满足性质。
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//记录pparent
Node* parentParent = parent->_parent;
//1、建立subR和parent之间的关系
parent->_parent = subR;
subR->_left = parent;
//2、建立parent和subRL之间的关系
parent->_right = subRL;
if (subRL)//判空,防止h为空时候 报错
subRL->_parent = parent;
//3、建立parentParent和subR之间的关系
if (parentParent == nullptr)
{
_root = subR;
subR->_parent = nullptr; //subR的_parent指向需改变
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subR;
}
else //parent == parentParent->_right
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
//4、更新平衡因子
subR->_bf = parent->_bf = 0;
}
右单旋并不会破环原本二叉搜索树的性质
参考左旋转理解
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//记录pparent
Node* parentParent = parent->_parent;
//1、建立subL和parent之间的关系
subL->_right = parent;
parent->_parent = subL;
//2、建立parent和subLR之间的关系
parent->_left = subLR;
if (subLR)//判空防止h为0的时候报错
subLR->_parent = parent;
//3、建立parentParent和subL之间的关系
if (parentParent == nullptr)//判断parent在旋转前是不是根节点
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else //parent == parentParent->_right
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
//4、更新平衡因子
subL->_bf = parent->_bf = 0;
}
左右双旋的步骤如下:
1.以subL为旋转点进行左单旋。
2.以parent为旋转点进行右单旋。
3.更新平衡因子。
左右双旋后满足二叉搜索树的性质:
就是左旋后右旋,上面解释了左旋和右旋都不影响性质,这只是连起来加了个先后顺序。
左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:
1.当subLR插入后平衡因子变为-1,更新后parent、subL、subLR的平衡因子分别更新为1、0、0。
2、当subLR插入后平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。
3、当subLR插入后平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0
//左右双旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//提前记录subLR的平衡因子
int bf = subLR->_bf; //subLR不可能为nullptr,因为subL的平衡因子是1
//1、以subL为旋转点进行左单旋
RotateL(subL);
//2、以parent为旋转点进行右单旋
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); //在旋转前树的平衡因子就有问题
}
}
右左双旋的步骤如下:
1.以subR为旋转点进行右单旋。
2.以parent为旋转点进行左单旋。
3.更新平衡因子。
右左双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:
1、当subRL原始平衡因子是1时,左右双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。
2、当subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。
3、当subRL原始平衡因子是0时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、0、0。
//右左双旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//提前记录subLR的平衡因子
int bf = subLR->_bf; //subLR不可能为nullptr,因为subL的平衡因子是1
//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); //在旋转前树的平衡因子就有问题
}
}
经过旋转后,不平衡的树变得平衡了,而上面的树本来就是平衡的,所以不需要再向上更新平衡因子。
总的插入:
//插入函数
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr) //若为空树,则插入结点直接作为根结点
{
_root = new Node(kv);
return true;
}
//1、按照二叉搜索树的插入方法,找到待插入位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
{
//往该结点的左子树走
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
{
//往该结点的右子树走
parent = cur;
cur = cur->_right;
}
else //待插入结点的key值等于当前结点的key值
{
//插入失败(不允许key值冗余)
return false;
}
}
//2、将待插入结点插入到树中
cur = new Node(kv); //根据所给值构造一个新结点
if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值
{
//插入到parent的左边
parent->_left = cur;
cur->_parent = parent;
}
else //新结点的key值大于parent的key值
{
//插入到parent的右边
parent->_right = cur;
cur->_parent = parent;
}
//3、更新平衡因子,如果出现不平衡,则需要进行旋转
while (cur != _root) //最坏一路更新到根结点
{
if (cur == parent->_left) //parent的左子树增高
{
parent->_bf--; //parent的平衡因子--
}
else if (cur == parent->_right) //parent的右子树增高
{
parent->_bf++; //parent的平衡因子++
}
//判断是否更新结束或需要进行旋转
if (parent->_bf == 0) //更新结束(新增结点把parent左右子树矮的那一边增高了,此时左右高度一致)
{
break; //parent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
}
else if (parent->_bf == -1 || parent->_bf == 1) //需要继续往上更新平衡因子
{
//parent树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == -2 || parent->_bf == 2) //需要进行旋转(此时parent树已经不平衡了)
{
if (parent->_bf == -2)
{
if (cur->_bf == -1)
{
RotateR(parent); //右单旋
}
else //cur->_bf == 1
{
RotateLR(parent); //左右双旋
}
}
else //parent->_bf == 2
{
if (cur->_bf == -1)
{
RotateRL(parent); //右左双旋
}
else //cur->_bf == 1
{
RotateL(parent); //左单旋
}
}
break; //旋转后就一定平衡了,无需继续往上更新平衡因子(旋转后树高度变为插入之前了)
}
else
{
assert(false); //在插入前树的平衡因子就有问题
}
}
return true; //插入成功
}
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
1.验证其为二叉搜索树
如果中序遍历得到一个有序的序列,就说明为二叉搜索树
//中序遍历
void Inorder()
{
_Inorder(_root);
}
//中序遍历子函数
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << " ";
_Inorder(root->_right);
}
2.验证其为平衡树
每个结点子树高度差的绝对值不超过1(注意结点中如果没有平衡因子)
节点的平衡因子是否计算正确
利用递归去验证,先判断左子树是否是平衡二叉树,再判断右子,若左右子树都为平衡二叉树,则返回当前子树的高度的上一层,继续去判断上一层,直到判断到跟为止。
//判断是否为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; //平衡二叉树的条件
}
跟二叉搜索树的查找方式一样:
//查找函数
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first) //key值小于该结点的值
{
cur = cur->_left; //在该结点的左子树当中查找
}
else if (key > cur->_kv.first) //key值大于该结点的值
{
cur = cur->_right; //在该结点的右子树当中查找
}
else //找到了目标结点
{
return cur; //返回该结点
}
}
return nullptr; //查找失败
}
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效****的时间复杂度,即 l o g 2 ( N ) log_2(N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数 、据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。