目录
3. 新节点插入较高左子树的右侧,进行左右双旋:先左单旋,再右单旋
4. 新节点插入较高右子树的左侧,进行右左双旋:先右单旋,再左单旋
AVL树的概念:
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下。
故,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis(AVL树名字的由来)在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1. 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
2. 它的左右子树都是AVL树
即,通过控制二叉搜索树中每颗子树的左右子树的高度差的绝对值不超过1,使得这颗二叉搜索树更接近于完全二叉树,高度接近log2N,提高查找效率。
AVL树结点定义:
template <class K, class V>
struct AVLTreeNode
{
AVLTreeNode(const pair<K, V>& kv)
: _left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0)
{ }
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
pair<K, V> _kv; // 键值对,AVL树的每个节点的元素类型
int _bf; // balance factor 平衡因子
};
上方AVLTreeNode定义中,每个结点包含三个指针,左右子节点指针以及双亲结点指针,结点数据采取的键值对pair。其中_bf为balance factor平衡因子,这个不是定义AVL树必须的,只是这里采取了平衡因子的实现方法,或许更简便。
平衡因子:
结点的平衡因子 _bf = 右子树的高度 - 左子树的高度
AVL树的插入:
bool Insert(const pair<K, V>& kv)
{
// 如果此时是一个空树,直接建立新节点,改_root即可。此时左右子节点和父节点均为nullptr,平衡因子为0.
if(_root == nullptr)
{
_root = new Node(kv);
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==nullptr,parent为要插入位置的父节点
cur = new Node(kv);
if(kv.first > parent->_kv.first) {
parent->_right = cur;
}
else {
parent->_left = cur;
}
cur->_parent = parent;
// 现在整个树的父子结点关系都弄好了,新插入结点的左右子节点为nullptr,
// 更新平衡因子(parent->_bf)
while(parent != nullptr)
{
if(cur == parent->_right) {
parent->_bf++;
}
else {
parent->_bf--;
}
if(parent->_bf == 0) {
// 表示之前_bf == -1 or _bf == 1,此时parent这个树高度不变,不会影响其他祖宗结点。
break;
}
else if(abs(parent->_bf) == 1) {
// 之前_bf == 0,此时parent这棵树高度改变,需要继续向上修改
cur = parent;
parent = parent->_parent;
}
else if(abs(parent->_bf) == 2) {
// 需要旋转
// if(parent->_bf == 2 && parent->_right->_bf == 1) {
if(parent->_bf == 2 && cur->_bf == 1) {
// 较高右子树的右边高,左单旋
RotateL(parent);
}
else if(parent->_bf == -2 && parent->_left->_bf == -1) {
// 较高左子树的左边高,右单旋
RotateR(parent);
}
else if(parent->_bf == -2 && cur->_bf == 1) {
// 较高左子树的右边插入,左右双旋
RotateLR(parent);
}
else if(parent->_bf == 2 && cur->_bf == -1) {
// 较高右子树的左边插入,右左双旋
RotateRL(parent);
}
break; // 一次插入最多一次旋转。
}
else {
assert(false);
}
}
return true;
}
AVL树是二叉搜索树的一种扩展,基于二叉搜索树的性质,增添了每个结点的左右子树的高度差的绝对值不超过1,也就是平衡因子<=1的规定,以此保证搜索性能。
故,AVL树的插入与二叉搜索树的插入原理相同。这里就不赘述了(参照二叉搜索树的插入方式)。在将结点插入之后,最重要的是更新平衡因子,因为AVL树就是通过保证整棵树的每个结点的平衡因子从而确保树的高度以及效率的。
平衡因子更新:
新插入结点直接影响并改变父节点的平衡因子,可能改变根节点至该新增结点路径上的所有结点的平衡因子。
平衡因子更新实现代码为上方Insert函数最后的那个while循环
(以下用cur代表新增结点,parent代表新增结点的父节点)
1. 若cur 是 parent的左孩子,parent的平衡因子--
2. 若cur 是 parent的右孩子,parent的平衡因子++
根据parent的更新后的平衡因子,决定下一步操作。
1. 若parent->_bf == 0,则结束平衡因子的更新。
原因:更新前_bf == 1 or -1(注意任何一个结点的原始平衡因子都不可能是2 or -2),此时插入后,parent这颗树的高度不变,不影响parent的parent(基于parent不是根节点)。注意:新增结点的父节点要么只有一个子节点要么没有子节点。
2. 若parent->_bf == 1 or -1 则继续向上更新,即执行cur = parent; parent = parent->_parent;
原因:原本parent->_bf == 0,此次插入新节点后,parent这颗子树高度改变,影响了parent的父节点,故需要循环更新。
3. 若parent->_bf == 2 or -2,表示此时parent这颗子树已经不平衡了,需要进行旋转调整。且旋转之后,平衡因子更新结束,原因见旋转的作用与结果。
4. 若abs(parent->_bf) >= 2,则表示这颗树在新增结点前就出现了问题,正常情况不会出现。
AVL树的旋转:
我们这里说的旋转,是因为某个结点的bf == 2 or -2,也就是这个结点的左右子树高度差超过了1,不平衡了,故需要旋转。而不平衡一定是由插入某个结点导致的。
根据插入结点位置的不同,将AVL树的旋转分为4类: