什么是AVL树?
首先,我们来回顾一下二叉搜索树,我们知道,二叉搜索树虽然可以缩短查找的效率,但是如果数据有序或接近有序,二叉搜索树将退化为单支树,查找元素的效率相当于在顺序表中搜索元素,效率很低。因此,两位俄罗斯数学家G.M.Adelson-Velskii和E.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新节点后,如果能保证每个结点的左右子树高度差的绝对值不超过1,即可降低树的高度,从而减少平均搜索次数。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树。
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在log2(N),搜索时的时间复杂度为O(log2(N))。
AVL树的性能:
AVL树是一棵绝对平衡的二叉搜索树,其要求每个结点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2(N)。
但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置,即旋转的量级为O(log2(N))。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
AVL树结点的定义
// AVL树结点
template<class K, class V>
struct AVLTreeNode{
AVLTreeNode(const pair<K, V>& kv)
: _bf(0)
, _kv(kv)
, _parent(nullptr)
, _left(nullptr)
, _right(nullptr)
{}
// 平衡因子
int _bf;
pair<K, V> _kv;
// 该节点双亲
AVLTreeNode<K, V>* _parent;
// 该节点左孩子
AVLTreeNode<K, V>* _left;
// 该节点右孩子
AVLTreeNode<K, V>* _right;
};
AVL树的插入
从上面AVL数结点的定义,可以看出,AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成二叉搜索树。那么AVL树的插入过程可以分为以下步骤:
- 按照二叉搜索树的方式将新节点插入到AVL树中。
- 新节点插入后,AVL树的平衡性可能会遭到破坏,此时,就需要调整平衡因子,并检测是否破坏了AVL树的平衡性。
- cur插入后,cur的parent的平衡因子一定要调整,在插入之前,cur的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的平衡因子违反平衡树的性质,需要对其进行旋转处理。
// 插入
bool insert(const pair<K, V>& kv){
// 空树,直接插入
if (_root == nullptr){
_root = new Node(kv);
_root->bf = 0;
return true;
}
// 保存父指针
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr){
// 大于根节点,右树找
if (kv.first > cur->_kv.first){
parent = cur;
cur = cur->_right;
}
// 小于根节点,左树找
else if (kv.first < cur->_kv.first){
parent = cur;
cur = cur->_left;
}
// 已经存在,插入失败
else{
return false;
}
}
// 找到插入位置
cur = new Node(kv);
// 新插入结点比parent大,插入到parent右树
if (kv.first > parent->_kv.first){
parent->_right = cur;
cur->_parent = parent;
}
// 新插入结点比parent小,插入到parent左树
else{
parent->_left = cur;
cur->_parent = parent;
}
// 调整平衡因子
while (parent){
// 新插入在parent的右树,平衡因子加1
if (cur == parent->_right){
++parent->_bf;
}
// 新插入在parent的左树,平衡因子减1
else{
--parent->_bf;
}
// 平衡因子为0,高度无影响
if (parent->_bf == 0){
break;
}
// 平衡因子为1,高度变了,向上更新
else if (abs(parent->_bf) == 1){
cur = parent;
parent = parent->_parent;
}
// 平衡因子绝对值为2,不平衡,旋转调整
else if (abs(parent->_bf) == 2){
// 平衡因子为2,右树变高
if (parent->_bf == 2){
// 当前为1,新节点插入在较高右子树的右侧,左单旋
if (cur->_bf == 1){
leftRotate(parent);
}
// 当前为-1,新节点插入在较高右子树的左侧,右左单旋
else if (cur->_bf == -1){
rightLeftRotate(parent);
}
}
// 平衡因子为-2,左树变高
else if (parent->_bf == -2){
// 当前为-1,新节点插入在较高左子树的左侧,右单旋
if (cur->_bf == -1){
rightRotate(parent);
}
// 当前为1,新节点插入在较高左子树的右侧,左右单旋
else if (cur->_bf == 1){
rightLeftRotate(parent);
}
}
break;
}
// 平衡因子为其他值,出错
else{
assert(false);
}
}
}
AVL树的旋转
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据结点插入位置的不同,AVL树的旋转分为四种:
- 新节点插入较高左子树的左侧–左左:右单旋。
上图在插入前,AVL树是平衡的,新节点插入到10的左子树中,10的左子树高度增加1,导致以20为根的二叉树不平衡,要想让20为根的二叉树平衡,只能将20的左子树高度减少一层,20的右子树增加一层。
进行右单旋:将左子树往上提,这样20转下来,因为20比10大,只能将其放在10的右树,而如果10有右树,右树的根一定大于10小于20,因此将其放在20的左树,旋转完成后,更新结点的平衡因子即可。
注意事项:
- 10结点的右孩子可能存在,也可能不存在。
- 20可能是根节点,也可能是子树。
如果是根节点,旋转完成后,要更新根节点。
如果是子树,可能是某个节点的左子树,也可能是右子树。
// 右单旋
void rightRotate(Node* parent){
// 20结点的左树10
Node* subL = parent->_left;
// 20结点的左树的右树b
Node* subLR = parent->_left->_right;
// 将b挂到20结点的左树
parent->_left = subLR;
// 如果b不为空,将b的双亲结点指向20结点
if (subLR){
subLR->_parent = parent;
}
// 将20结点挂到10结点的右树
subL->_right = parent;
// pp保存20结点的双亲结点
Node* pp = parent->_parent;
// 将20结点的双亲结点改为10结点
parent->_parent = subL;
// 如果20结点是根节点
if (pp == nullptr){
// 10结点设为根节点
_root = subL;
_root->_parent = nullptr;
}
// 20结点不是根节点
else{
// 20结点是其双亲结点的左子树
if (pp->_left == parent){
pp->_left = subL;
}
// 20结点是其双亲结点的右子树
else{
pp->_right = subL;
}
// 10结点的双亲结点改为20结点的双亲结点
subL->_parent = pp;
}
// 旋转完成后,平衡因子更新为0
parent->_bf = subL->_bf = 0;
}
- 新节点插入较高右子树的右侧–右右:左单旋。
同样,上图在插入前是平衡的,新节点插入到20节点的右树,导致20节点的右树高度增加1,从而导致20节点的平衡因子增加到1,10节点的平衡因子增加到2,导致不平衡,需要进行调整,使其达到平衡。
进行左单旋调整:将10节点压下去,因为20节点的左树都是大于10,小于20的,所以可以将20结点的左树调整到10节点的右树,同时将10节点调整为20节点的左树。即可达到平衡,旋转完成后,更新平衡因子。
注意事项:
- 20节点的左孩子可能存在,也可能不存在。
- 10节点可能是根节点,也可能是子树。
如果10节点是根节点,旋转完成后,要更新根节点。
如果是子树,可能是某个节点的左子树,也可能是右子树。
// 左单旋
void leftRotate(Node* parent){
// 10结点的右树20
Node* subR = parent->_right;
// 10结点的右树的左树b
Node* subRL = parent->_right->_left;
// 将b设置为10结点的右树
parent->_right = subRL;
// 如果b非空,将b的父亲设置为10结点
if (subRL){
subRL->_parent = parent;
}
// 保存10结点的父结点
Node* pp = parent->_parent;
// 将10结点的父结点设置为结点20
parent->_parent = subR;
// 将10结点设置为结点20的左树
subR->_left = parent;
// 10结点是根节点
if (parent == _root){
// 20结点设为根节点
_root = subR;
_root->_parent = nullptr;
}
// 10结点不是根结点
else{
// 10结点是其父结点的左树
if (parent == pp->_left){
pp->_left = subR;
subR->_parent = pp;
}
// 10结点是其父结点的右树
else{
pp->_right = subR;
subR->_parent = pp;
}
}
// 调整平衡因子
parent->_bf = subR->_bf = 0;
}
- 新节点插入较高左子树的右侧–左右:先左单旋再右单旋。
上图在插入前是平衡的,但是20结点的左树插入之后,导致20结点的高度增加1,从而影响10结点,最终影响30结点,30结点平衡因子为-2,不平衡,需要调整。
进行双旋,先进行左单旋,再进行右单旋。将20结点的左树挂到10结点的右树,再将10结点挂到20结点的左树,完成左单旋;然后将20结点的右树挂到30结点的左树。在将30结点挂到20结点的右树,完成右单旋,树达到平衡。
注意事项:
- 这里要单独考虑平衡因子,如果不单独考虑平衡因子,左右单旋后,三个结点的平衡因子都是0,从图上看,三个结点的平衡因子显然不全为0。
- 上述情况,如果新插入的结点在b,则双旋之后,30结点的平衡因子应该为1,其他两个节点平衡因子为0,因为插入在b,c的高度为h-1,双旋后,c会成为30结点的左树,从而导致30结点的平衡因子为1。
- 如果新插入的结点在c,则双旋之后,10结点的平衡因子为-1,其他两个节点的平衡因子为0,因为插入在c,b的高度为h-1,双旋之后,b会成为10结点的右树,从而导致10结点的平衡因子为-1。
// 左右双旋
void leftRightRotate(Node* parent){
// 保存20结点的平衡因子,旋转完成后
// 根据该平衡因子对其他平衡因子进行调整
int bf = parent->_left->_right->_bf;
// 对10结点所在子树进行左单旋
leftRotate(parent->_left);
// 对30结点所在树进行右单旋
rightRotate(parent);
// 插入后20结点平衡因子为1
// 说明新插入结点在20结点的右树,说明20结点左树高度较低
// 左右旋之后,20结点的左树成为10结点的右树
// 从而10结点的平衡因子为-1
if (bf == 1){
parent->_left->_bf = -1;
}
// 插入后20结点平衡因子为-1
// 说明新插入结点在20结点的左树
// 20结点的右树高度较低
// 左右旋之后,20结点的右树成为30结点的左树
// 从而30结点的平衡因子为1
else if (bf == -1){
parent->_bf = 1;
}
}
- 新节点插入较高右子树的左侧–右左:先右单旋再左单旋。
同样,上图插入在c,插入之后20结点右子树高度增加,从而向上影响,直到影响到10结点,使得10结点平衡因子为2,导致不平衡,双旋进行调整。
首先进行右单旋,将c挂到30结点的左,再将30加点挂到20结点的有,完成右单旋;然后进行左单旋,将b挂到10结点的右,然后将10结点挂到20结点的左,完成左单旋。
注意事项:
- 这里要注意平衡因子需要单独处理。
- 如果新节点插入到b,则20的右树c的高度较低,为h-1,双旋之后,c会成为30结点的左树,从而导致30结点的平衡因子为1,其他两个节点平衡因子为0。
- 如果新节点插入到c,则20结点的左树b的高度较低,为h-1,双旋之后,b会成为10结点的右树,从而导致10结点的平衡因子为-1。
// 右左旋
void rightLeftRotate(Node* parent){
// 首先保存插入之后20结点的平衡因子
int bf = parent->_right->_left->_bf;
// 进行右单旋
rightRotate(parent->_right);
// 进行左单旋
leftRotate(parent);
// 新节点插入在c
// b的高度较低为h-1
// 双旋之后,b会成为10结点的右树
// 因此10结点的平衡因子为-1
if (bf == 1){
parent->_bf = -1;
}
// 新节点插入在b
// c的高度较低为h-1
// 双旋之后,c会成为30结点的左树
// 因此30结点的平衡因子为1
else if (bf == -1){
parent->_right->_bf = 1;
}
}
AVL树旋转总结:
假如以parent为根的子树不平衡,即parent的平衡因子为2或-2,分以下情况考虑:
- parent的平衡因子为2,说明parent的右子树高,设parent的右子树的根为subR。
当subR的平衡因子为1时,执行左单旋;
当subR的平衡因子为-1时,执行右左双旋; - parent的平衡因子为-2,说明parent的左子树高,设parent的左子树的根为subL。
当subL的平衡因子为-1时,执行右单旋;
当subL的平衡因子为1时,执行左右双旋;
旋转完成后,原parent为根的子树高度降低,已经平衡,不需要再向上更新。
AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡的限制,所以要验证一棵树是否为AVL树,只需要验证两点即可:
- 这棵树是否为二叉搜索树。
- 这棵树是否平衡。
如果两个条件都满足就说明是AVL树。
首先来验证其是否为二叉搜索树:
中序遍历结果为有序即为二叉搜索树。
// 中序
void _inorder(Node* root){
_inorder(root->_left);
cout << root->_kv.first << " ";
_inorder(root->_right);
}
// 中序遍历
void inorder(){
_inOrder(_root);
cout << endl;
}
验证是否平衡:
// 计算树的高度
int _height(Node* root){
if (root == nullptr){
return 0;
}
if (root->_left == nullptr && root->_right == nullptr){
return 1;
}
// 左树高度
int left_height = _height(root->_left) + 1;
// 右树高度
int right_height = _height(root->_right) + 1;
// 左树右树中高度的最大值为这棵树的高度
return left_height > right_height ? left_height : right_height;
}
// 是否平衡
bool _isBalance(Node* root){
// 空树是平衡的
if (root == nullptr){
return true;
}
// 计算平衡因子
int left_height = _height(root->_left);
int right_height = _height(root->_right);
int bf = right_height - left_height;
// 计算出的平衡因子和root的平衡因子不同
// 或者平衡因子绝对值超过1,不平衡
if (bf != root->_bf || abs(bf) > 1){
return false;
}
// root的左树和右树是否平衡
return _isBalance(root->_left) && _isBalance(root->_right);
}