概念
二叉搜索树在有序或者接近有序的情况下二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: O ( N 2 ) O(\frac{N}{2}) O(2N)
因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
因此AVL树也是由这两位数学家的名字命名的:
AVL树和空树都是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
一颗高度平衡的二叉搜索树可以就是AVL树,其高度和搜索效率都能保证在 O ( l o g 2 n ) O(log_2^n) O(log2n),
AVL树的节点
需要记录左右子树的高度差(即平衡因子),新插入的节点的平衡因子是0,因为AVL树涉及向上更新平衡因子,所以记录父节点。
template<typename K, typename V>
struct AVLNode
{
AVLNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
AVLNode<K, V>* _left;
AVLNode<K, V>* _right;
AVLNode<K, V>* _parent;
pair<K, V> _kv;
int _buf = 0;
};
AVL的插入(重头戏)
空树插入
// 空树的插入
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
正常插入
正常插入前需要先找到插入的位置,如果存在该节点即插入失败,走到NULL就是需要插入的位置,同时需要记录父节点:
// 非空树的插入
Node* parent = nullptr;
Node* cur = _root;
// 寻找插入的位置
while (cur)
{
parent = cur;
if (kv > cur->_kv)
{
cur = cur->_right;
}
else if (kv < cur->_kv)
{
cur = cur->_left;
}
else // 找到重复元素,插入失败
{
return false;
}
}
// 开始插入
cur = new Node(kv);
if (kv > parent->_kv)
{
cur->_parent = parent;
parent->_right = cur;
}
else
{
cur->_parent = parent;
parent->_left = cur;
}
更新平衡因子
新节点插入后要保证AVL树的特性,平衡因子就是衡量的标准:
cur插入后,parent的平衡因子一定需要调整,在插入之前,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的平衡因子违反平衡树的性质,需要对其进行旋转处理
while (parent)
{
// 1. 更新平衡因子
if (parent->_left == cur)
parent->_buf--;
else
parent->_buf++;
// 2. 根据parent的平衡因子进行分别处理
if (parent->_buf == 0) // 插入成功
{
break;
}
else if (abs(parent->_buf) == 1) // 继续往上更新
{
cur = parent;
parent = parent->_parent;
}
else if (abs(parent->_buf) == 2) // 不平衡开始旋转
{
.....
break;
}
else // 说明之前插入的节点有问题
{
assert(false);
}
}
AVL树的旋转
旋转分为四种:
总结:中间挑两边,单旋都为0,双旋看中间,中间为0即为0,非0双取反(方向和值)
代码:
if (parent->_buf == 2 && cur->_buf == 1) // 单右侧高,左单旋
{
RotateL(parent);
}
else if (parent->_buf == -2 && cur->_buf == -1) // 单左侧高, 右单旋
{
RotateR(parent);
}
else if (parent->_buf == -2 && cur->_buf == 1) // 左括号型,左右双旋(先左旋转subL转换为单左侧高,再右旋转parent)
{
RotateLR(parent);
}
else if (parent->_buf == 2 && cur->_buf == -1) // // 右括号型,右左双旋(先右旋转subR转换为单左侧高,再左旋转parent)
{
RotateRL(parent);
}
else
{
assert(false);
}
左单旋
// 左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pparent = parent->_parent;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
parent->_parent = subR;
subR->_left = parent;
subR->_parent = pparent;
if (pparent)
{
if (pparent->_left == parent)
pparent->_left = subR;
else
pparent->_right = subR;
}
else
_root = subR;
parent->_buf = subR->_buf = 0;
}
右单旋
// 右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* pparent = parent->_parent;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
parent->_parent = subL;
subL->_right = parent;
subL->_parent = pparent;
if (pparent)
{
if (pparent->_left == parent)
pparent->_left = subL;
else
pparent->_right = subL;
}
else
_root = subL;
parent->_buf = subL->_buf = 0;
}
左右双旋
// 左右双旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int buf = subLR->_buf;
RotateL(subL);
RotateR(parent);
subLR->_buf = 0;
if (buf == -1)
{
subL->_buf = 0;
parent->_buf = 1;
}
else if (buf == 1)
{
parent->_buf = 0;
subL->_buf = -1;
}
else if (buf == 0)
{
parent->_buf = subL->_buf = 0;
}
else
{
assert(false);
}
}
右左双旋
// 右左双旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int buf = subRL->_buf;
RotateR(subR);
RotateL(parent);
subRL->_buf = 0;
if (buf == 1)
{
parent->_buf = -1;
subR->_buf = 0;
}
else if (buf == -1)
{
parent->_buf = 0;
subR->_buf = 1;
}
else if (buf == 0)
{
parent->_buf = subR->_buf = 0;
}
else
{
assert(false);
}
}
AVL树的验证
AVL树的验证需要验证两步:
- 符合二叉搜索树的特性(中序遍历是有序的)
- 左右子树高度差不超过1
第一步很好验证,只要按照二叉搜索树进行插入就不会出错,基本上不需要刻意验证。但是第二步需要单独验证一下:
bool IsBalance()
{
return _IsBalance(_root);
}
bool _IsBalance(Node* root)
{
if (root == nullptr)
return true;
int leftH = Hight(root->_left);
int rightH = Hight(root->_right);
int diff = abs(leftH - rightH);
if (diff > 2)
return false;
return _IsBalance(root->_left) && _IsBalance(root->_right);
}
int Hight(Node* root)
{
if (root == nullptr)
return 0;
return max(Hight(root->_left), Hight(root->_right)) + 1;
}
AVL树的删除
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不过与二叉搜索树不同的是,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
有兴趣参考这篇博客:二叉树的删除
AVL树性能分析
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
样可以保证查询时高效的时间复杂度,即
l
o
g
2
(
N
)
log_2 (N)
log2(N)。
但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。