AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
AVL树节点的定义
AVL树的结点比二叉搜索树的结点多了指向父结点的指针和储存平衡因子的变量
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;// 平衡因子=右树高度-左树高度
AVLTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _bf(0)
{ }
};
AVL树的插入
AVL树的插入过程可以分为两步:
- 按照二叉搜索树的方式插入结点
- 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了AVL树的平衡性
平衡因子的操作如下
子结点插入后,父结点的平衡因子需要调整,在插入之前,父结点的平衡因子为三种情况之一:-1,0, 1, 插入又分以下两种情况
- 子结点插入到父节点的右方,父节点的平衡因子加1
- 子结点插入到父节点的左方,父节点的平衡因子减1
插入结点后,父结点的平衡因子可能为0,正负1,正负2
- 如果父节点的平衡因子为0,说明插入之前父节点的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功。
- 如果父节点的平衡因子为正负1,说明插入前父节点的平衡因子一定为0,插入后被更新成正负1,此时以父节点为根的树的高度增加,需要继续向上更新。
- 如果父节点的平衡因子为正负2,则父节点的平衡因子违反平衡树的性质,需要对其进行旋转处理。
bool Insert(const pair<K, V>& kv)
{
// 空树直接插入结点
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
// 找到要插入结点的位置
Node* parent = nullptr;
Node* cur = _root;
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 < cur->_kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
// 控制树的平衡
while (parent)
{
// 更新平衡因子
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
// 检查父亲的平衡因子
// 1、父亲所在子树的高度不变,不影响祖先,更新结束
if (parent->_bf == 0)
{
break;
}// 2、父亲所在子树的高度变了,继续往上更新
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}// 3、父亲所在子树的出现了不平衡,需要旋转处理
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 旋转更新结点
}
else
{
assert(false);
}
}
return true;
}
AVL树的旋转
在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡。根据节点插入位置的不同,AVL树的旋转分为四种。
右单旋
新节点插入较高左子树的左侧,如图
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
// 排除结点为空的情况
if (subLR)
subLR->_parent = parent;
// 记录根结点的父节点
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
// 更新子树的根结点
if (parent==_root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
ppNode->_left = subL;
else
ppNode->_right = subL;
subL->_parent = ppNode;
}
// 调整平衡因子
parent->_bf = subL->_bf = 0;
}
左单旋
新节点插入较高右子树的右侧,如图
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
// 排除结点为空的情况
if (subRL)
subRL->_parent = parent;
// 记录根结点的父节点
Node* ppNode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
// 更新子树的根结点
if (parent==_root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
ppNode->_left = subR;
else
ppNode->_right = subR;
subR->_parent = ppNode;
}
// 调整平衡因子
subR->_bf = parent->_bf = 0;
}
先左单旋再右单旋
新节点插入较高左子树的右侧
这里可以直接调用上文的左单旋和右单旋,但是需要注意平衡因子的调整
这里分又为3种情况
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
// 左单旋
RotateL(parent->_left);
// 右单旋
RotateR(parent);
// 调节平衡因子
if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == 0)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
先右单旋再左单旋
新节点插入较高右子树的左侧
这里也分3中情况和上文类似
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
// 右单旋
RotateR(parent->_right);
// 左单旋
RotateL(parent);
// 调节平衡因子
if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
总结调节平衡因子的情况
while (parent)
{
// 更新平衡因子
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
// 检查父亲的平衡因子
// 1、父亲所在子树的高度不变,不影响祖先,更新结束
if (parent->_bf == 0)
{
break;
}// 2、父亲所在子树的高度变了,继续往上更新
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}// 3、父亲所在子树的出现了不平衡,需要旋转处理
else if (parent->_bf == 2 || parent->_bf == -2)
{
if (parent->_bf == -2 && cur->_bf == -1)
{
// 右旋
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == 1)
{
// 左旋
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
else
{
assert(false);
}
break;
}
else
{
assert(false);
}
}
AVL树的验证
为了检验AVL的正确性我们添加检验平衡因子的功能
int Height(Node* root)
{
if (root == nullptr)
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 == nullptr)
return true;
// 记录左右子树的高度
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
// 不满足条件报错
if (rightHeight - leftHeight != root->_bf)
{
cout << "平衡因子异常:" << root->_kv.first << endl;
}
// 判断其左右子树是否满足条件
return abs(rightHeight - leftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);
}
bool IsBalance()
{
return _IsBalance(_root);
}
AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。