一、AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。也就是所谓的AVL树。
AVL树的性质
a:它的左右子树都是AVL树
b:左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度O(logN)。
二、AVL树的实现
1、结点的定义
template<class T>
struct AVLNode {
AVLNode(T key = T())
:left(nullptr)
, right(nullptr)
,parent(nullptr)
, _key(key)
, bf(0);
{}
T _key;
int bf;//平衡因子
AVLNode<T>* left;
AVLNode<T>* right;
AVLNode<T>* parent;//该结点的父结点(双亲)
};
这里要用三叉链的原因是方便向上找祖先的平衡因子。 (也可以用栈来存储平衡因子,这样就不用三叉链了)。
2、AVL树的插入
分析:
插入结点会影响部分祖先结点的平衡因子,所以要更新平衡因子。
这里我定义的高度差是右树减左树,所以插入结点父结点的平衡因子与插入结点的位置有关。
插在左子树平衡因子 -1;插在右子树平衡因子+1;
至于是否继续向上更新祖先的平衡因子,是要看以插入结点的父结点作为根的子树的高度是否会变化。
1、parent的平衡因子==0
说明parent的平衡因子更新前是 1 or -1,插入结点插入在矮的一侧,此时parent所在子树高度不变,不需要向上更新平衡因子。
2、parent的平衡因子==1 or -1
说明parent的平衡因子更新前是0,插入结点插入在parent的左右任意一侧,此时parent所在子树的高度发生变化,需要向上更新平衡因子。
3、parent的平衡因子== 2 or -2
说明parent的平衡因子更新前是1 or -1,插入结点插入在高的一侧,进一步加剧了左右高度差,此时需要旋转来操作平衡。
大体框架:
1、除去旋转的大体框架
bool insert(const T& key)
{
if (_root == nullptr)//空树的情况
{
_root = new Node(key);
return true;
}
Node* pcur = _root;
Node* pre = nullptr;
while (pcur)//插入树里面不存在的值,pcur一定会走向空
{
pre = pcur;//插入位置的父节点
if (pcur->_key < key)
{
pcur = pcur->right;
}
else if (pcur->_key > key)
{
pcur = pcur->left;
}
else//插入的值已存在,无需插入
{
return false;
}
}
pcur = new Node(key);//作为插入的新结点
if (pre->_key < key)
pre->right = pcur;
else
pre->left = pcur;
pcur->parent = pre;//链接父子结点
// 插入完成后更新并检查平衡因子,不满足平衡条件的,旋转
while (pre)
{
//更新平衡因子
if(pre->right == pcur)
++pre->bf;//插在右子树平衡因子+1
if(pre->left == pcur)
--pre->bf;//插在左子树平衡因子 -1
//更新后检查平衡因子
if (pre->bf == 0)
break;
else if (pre->bf == -1 || pre->bf == 1)//插入前平衡因子是0,插入后改变了树的高度
{
pcur = pre;//pcur=pcur->parent
pre = pre->parent;//向上继续判断
}
else if (pre->bf == 2 || pre->bf == -2)//插入前平衡因子不是1就是-1
{
//旋转处理
//注意旋转处理完了要跳出循环
break;//此时要跳出循环,(因为旋转后,原来根结点的位置发生改变,且此时平衡因子出现问题的那棵子树达到平衡),不然会出问题(因为这个调试了半天)
}
else
assert(false);//平衡因子绝对值大于2说明旋转操作有问题,此时断言错误
}
return true;
}
2、旋转的分析
情况一:单纯的左子树高(插在subL的左子树上)
此时需要右单旋来达到平衡:
if (pre->bf == -2 && pcur->bf == -1)//左子树高
RotateR(pre);
//右单旋(左子树高)此时插入结点的位置是subL的左子树上
void RotateR(Node* root)
{
Node* subL = root->left;//左子树的根结点
Node* subLR = subL->right;
root->left = subLR;
if (subLR)//不为空则链接
{
subLR->parent = root;
}
subL->right = root;
if (root->parent == nullptr)//整个二叉树的根结点出现问题
{
_root = subL;//更新根结点
}
else//更换了子树根的位置,需要重新链接,避免出现4插链的情况
{
if (root == root->parent->left)
root->parent->left = subL;
if(root == root->parent->right)
root->parent->right = subL;
}
subL->parent = root->parent;
root->parent = subL;
//更新平衡因子
subL->bf = 0;
root->bf = 0;
}
情况二:单纯的右子树高(插在subR的右子树上)
此时需要左单旋来达到平衡:
else if(pre->bf == 2 && pcur->bf == 1)//右子树高
RotateL(pre);
//左单旋(右子树高)此时插入结点的位置是subR的右子树上
void RotateL(Node* root)
{
Node* subR = root->right;
Node* subRL = subR->left;
root->right = subRL;
if (subRL)//不为空
{
subRL->parent = root;
}
subR->left = root;
if (root->parent == nullptr)//整个二叉树的根结点出现问题
{
_root = subR;//更新根结点
}
else//更换了子树根的位置,需要重新链接,避免出现4插链的情况
{
if (root == root->parent->left)
root->parent->left = subR;
if (root == root->parent->right)
root->parent->right = subR;
}
subR->parent = root->parent;
root->parent = subR;
//更新平衡因子
root->bf = 0;
subR->bf = 0;
}
情况三:左子树高,但是插在subL的右子树上
此时需要先以subL(pcur)为根左旋,然后再以root(pre)为根右旋来达到平衡:
1、以subL(pcur)为根左旋:
2、以root(pre)为根右旋:
整个具体的过程:
这里的平衡因子还有两种情况要分开讨论:
(1)add插在subLR的左子树上(subLR->bf==-1):此时两次旋转完成后subL的平衡因子为0,root的平衡因子为1
(2)add插在subLR的右子树上(subLR->bf==1):此时两次旋转完成后subL的平衡因子为-1,root的平衡因子为0
else if (pre->bf == -2 && pcur->bf == 1)//左子树高但是插在subL的右子树上
{
RotateLR(pre);
}
//左旋+右旋(左子树高但是插在subL的右子树上)
void RotateLR(Node* root)
{
Node* subL = root->left;
Node* subLR = subL->right;
int bf = subLR->bf;//记录,用于判断更新情况
RotateL(subL);
RotateR(root);
if (bf == 0)
{
root->bf = 0;
subL->bf = 0;
subLR->bf = 0;
}
else if (bf == -1)
{
root->bf = 1;
subL->bf = 0;
subLR->bf = 0;
}
else if (bf == 1)
{
root->bf = 0;
subL->bf = 0;
subLR->bf = 1;
}
else
assert(false);
}
情况四:右子树高但插在subR的左子树上
此时需要先以subR(pcur)为根右旋,然后再以root(pre)为根左旋来达到平衡:
1、以subR(pcur)为根右旋:
2、以root(pre)为根左旋:
整个具体的过程:
这里的平衡因子还有两种情况要分开讨论:
(1)add插在subRL的左子树上(subRL->bf==-1):此时两次旋转完成后subR的平衡因子为1,root的平衡因子为0
(2)add插在subLR的右子树上(subRL>bf==1):此时两次旋转完成后subR的平衡因子为0,root的平衡因子为-1
else if (pre->bf == 2 && pcur->bf == -1)//右子树高但是插在subR的左子树上
{
RotateRL(pre);
}
//右旋+左旋(右子树高但是插在subR的左子树上)
void RotateRL(Node* root)
{
Node* subR = root->right;
Node* subRL = subR->left;
int bf = subRL->bf;
RotateR(subR);
RotateL(root);
if (bf == 0)
{
subR->bf = 0;
subRL->bf = 0;
root->bf = 0;
}
else if (bf == 1)
{
subR->bf = 0;
subRL->bf = 0;
root->bf = -1;
}
else if (bf == -1)
{
subR->bf = 1;
subRL->bf = 0;
root->bf = 0;
}
else
assert(false);
}
三、 AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
1. 验证其为二叉搜索树:
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树:
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子);节点的平衡因子是否计算正确
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftH = _Height(root->left);
int rightH = _Height(root->right);
return leftH > rightH ? leftH + 1 : rightH + 1;
}
bool IsBalance(Node* root)
{
if (nullptr == root)
return true;
int leftHeight = _Height(root->left);
int rightHeight = _Height(root->right);
int diff = rightHeight - leftHeight;
if (abs(diff) > 1)
{
cout << "高度差出现问题" << endl;
return false;
}
if (diff != root->bf)
{
cout << "平衡因子出现问题" << endl;
return false;
}
return IsBalance(root->left) && IsBalance(root->right);
}
测试用例:
int arr1[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
int arr2[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
源码:
#include<iostream>
#include<assert.h>
using namespace std;
template<class T>
struct AVLNode {
AVLNode(T key = T())
:left(nullptr)
, right(nullptr)
, parent(nullptr)
, _key(key)
, bf(0)
{}
T _key;
int bf;//平衡因子
AVLNode<T>* left;
AVLNode<T>* right;
AVLNode<T>* parent;
};
// AVL: 二叉搜索树 + 平衡因子的限制
template<class T>
class AVLTree {
public:
typedef AVLNode<T> Node;
AVLTree() = default;
~AVLTree()
{
Destroy(_root);
_root = nullptr;
}
void InOrder()
{
_InOrder(_root);//成员函数可以访问私有
}
int Height()
{
return _Height(_root);
}
Node* GetNode()//给外界提供获得私有结点的方式,便于调试
{
return _root;
}
bool IsBalance(Node* root)
{
if (nullptr == root)
return true;
int leftHeight = _Height(root->left);
int rightHeight = _Height(root->right);
int diff = rightHeight - leftHeight;
if (abs(diff) > 1)
{
cout << "高度差出现问题" << endl;
return false;
}
if (diff != root->bf)
{
cout << "平衡因子出现问题" << endl;
return false;
}
return IsBalance(root->left) && IsBalance(root->right);
}
bool find(T key)
{
Node* pcur = _root;
while (pcur)
{
if (pcur->_key < key)
{
pcur = pcur->right;
}
else if (pcur->_key > key)
{
pcur = pcur->left;
}
else
return true;
}
return false;
}
bool insert(const T& key)
{
if (_root == nullptr)//空树的情况
{
_root = new Node(key);
return true;
}
Node* pcur = _root;
Node* pre = nullptr;
while (pcur)//插入树里面不存在的值,pcur一定会走向空
{
pre = pcur;//插入位置的父节点
if (pcur->_key < key)
{
pcur = pcur->right;
}
else if (pcur->_key > key)
{
pcur = pcur->left;
}
else//插入的值已存在,无需插入
{
return false;
}
}
pcur = new Node(key);//作为插入的新结点
if (pre->_key < key)
pre->right = pcur;
else
pre->left = pcur;
pcur->parent = pre;//链接父子结点
// 插入完成后更新并检查平衡因子,不满足平衡条件的,旋转
while (pre)
{
//更新平衡因子
if(pre->right == pcur)
++pre->bf;//插在右子树平衡因子+1
if(pre->left == pcur)
--pre->bf;//插在左子树平衡因子 -1
//更新后检查平衡因子
if (pre->bf == 0)
break;
else if (pre->bf == -1 || pre->bf == 1)//插入前平衡因子是0,插入后改变了树的高度
{
pcur = pre;//pcur=pcur->parent
pre = pre->parent;//向上继续判断
}
else if (pre->bf == 2 || pre->bf == -2)//插入前平衡因子不是1就是-1
{
//旋转处理
if (pre->bf == -2 && pcur->bf == -1)//左子树高
RotateR(pre);
else if(pre->bf == 2 && pcur->bf == 1)//右子树高
RotateL(pre);
else if (pre->bf == -2 && pcur->bf == 1)//左子树高但是插在subL的右子树上
{
RotateLR(pre);
}
else if (pre->bf == 2 && pcur->bf == -1)//右子树高但是插在subR的左子树上
{
RotateRL(pre);
}
break;//此时要跳出循环,(因为旋转后,原来根结点的位置发生改变,且此时平衡因子出现问题的那棵子树达到平衡),不然会出问题(因为这个调试了半天)
}
else
assert(false);//平衡因子绝对值大于2说明旋转操作有问题,此时断言错误
}
return true;
}
private:
//右单旋(左子树高)此时插入结点的位置是subL的左子树上
void RotateR(Node* root)
{
Node* subL = root->left;//左子树的根结点
Node* subLR = subL->right;
root->left = subLR;
if (subLR)//不为空则链接
{
subLR->parent = root;
}
subL->right = root;
if (root->parent == nullptr)//整个二叉树的根结点出现问题
{
_root = subL;//更新根结点
}
else//更换了子树根的位置,需要重新链接,避免出现4插链的情况
{
if (root == root->parent->left)
root->parent->left = subL;
if(root == root->parent->right)
root->parent->right = subL;
}
subL->parent = root->parent;
root->parent = subL;
//更新平衡因子
subL->bf = 0;
root->bf = 0;
}
//左单旋(右子树高)此时插入结点的位置是subR的右子树上
void RotateL(Node* root)
{
Node* subR = root->right;
Node* subRL = subR->left;
root->right = subRL;
if (subRL)//不为空
{
subRL->parent = root;
}
subR->left = root;
if (root->parent == nullptr)//整个二叉树的根结点出现问题
{
_root = subR;//更新根结点
}
else//更换了子树根的位置,需要重新链接,避免出现4插链的情况
{
if (root == root->parent->left)
root->parent->left = subR;
if (root == root->parent->right)
root->parent->right = subR;
}
subR->parent = root->parent;
root->parent = subR;
//更新平衡因子
root->bf = 0;
subR->bf = 0;
}
//左旋+右旋(左子树高但是插在subL的右子树上)
void RotateLR(Node* root)
{
Node* subL = root->left;
Node* subLR = subL->right;
int bf = subLR->bf;//记录,用于判断更新情况
RotateL(subL);
RotateR(root);
if (bf == 0)
{
root->bf = 0;
subL->bf = 0;
subLR->bf = 0;
}
else if (bf == -1)
{
root->bf = 1;
subL->bf = 0;
subLR->bf = 0;
}
else if (bf == 1)
{
root->bf = 0;
subL->bf = 0;
subLR->bf = 1;
}
else
assert(false);
}
//右旋+左旋(右子树高但是插在subR的左子树上)
void RotateRL(Node* root)
{
Node* subR = root->right;
Node* subRL = subR->left;
int bf = subRL->bf;
RotateR(subR);
RotateL(root);
if (bf == 0)
{
subR->bf = 0;
subRL->bf = 0;
root->bf = 0;
}
else if (bf == 1)
{
subR->bf = 0;
subRL->bf = 0;
root->bf = -1;
}
else if (bf == -1)
{
subR->bf = 1;
subRL->bf = 0;
root->bf = 0;
}
else
assert(false);
}
void _InOrder(Node* root)//中序遍历
{
if (root == nullptr)
return;
_InOrder(root->left);
cout << root->_key << " ";
_InOrder(root->right);
}
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int leftH = _Height(root->left);
int rightH = _Height(root->right);
return leftH > rightH ? leftH + 1 : rightH + 1;
}
void Destroy(Node* root)//逐一销毁结点
{//后续删除
if (root == nullptr)
return;
Destroy(root->left);
Destroy(root->right);
delete root;
}
Node* _root=nullptr;
};
四、AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即logN。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。