AVL 树
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度O(logN)。
AVL树节点的定义
这里需要创建一个_parent和_bf,用来保存它的父亲结点(便于我们向上更新平衡因子)和平衡因子
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; //balance factor 平衡因子控制平衡
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0) //平衡因子初始化为0
{}
};
注意: 这里我们引入平衡因子来控制树的平衡,但是并不是只有这一种方式,引入平衡因子只是与其他方式相比较而言更好理解
AVL树的插入
AVL树插入结点时有以下三个步骤:
- 按照二叉搜索树的插入方法,找到待插入位置。
- 找到待插入位置后,将待插入结点插入到树中。
- 更新平衡因子,如果出现不平衡,则需要进行旋转。
因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:
- 待插入结点的key值比当前结点小就插入到该结点的左子树。
- 待插入结点的key值比当前结点大就插入到该结点的右子树。
- 待插入结点的key值与当前结点的key值相等就插入失败。
如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。
与二叉搜索树插入结点不同的是,AVL树插入结点后需要更新树中结点的平衡因子,因为插入新结点后可能会影响树中某些结点的平衡因子。
插入一个结点之后我们需要更新它父亲结点的平衡因子,以及沿着这条路径上的所有祖先结点都得判断它自身平衡因子是否发生改变
所以我们插入结点后需要倒着往上更新平衡因子直到根结点结束,更新规则如下:
- 新增结点在parent的右边,parent的平衡因子+ +。
- 新增结点在parent的左边,parent的平衡因子− −。
分析以上两种情况得出:
更新后_bf == 0,parent所在子树高度不变不会影响到,不会影响祖先,这时不用沿着_root的路径往上更新,这时插入结束
更新后_bf == 1/-1,parent所在子树高度不变,不会影响祖先,这时就需要沿着_root的路径往上更新
更新后_bf == 2/-2,parent所在子树高度发生变化且不平衡,对parent所在子树进行旋转,使它平衡,平衡之后,插入结束
平衡因子一直更新到根结点,插入结束
这里我根据情况三展开讲解一下旋转
AVL树的旋转
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构, 使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
旋转的时候需要注意:1.保持它是一颗二叉搜索树 2. 变成平衡树并且降低这棵树的高度
以下是一种抽象图,可能不太好理解
我这里举例来帮助理解
以下图示,新增结点之前是符合AVL树规则的子树
当高度为0时:
你可以在30的左边或者右边新增结点
当高度为1时
你可以在30的左右子树的左边/右边新增结点
当高度为2时
满足AVL树触发旋转的到高度为2时已经有36种情况了
当高度为3时大家可以自己算算有多少种情况,我这里就不举例了
1.新节点插入较高左子树的左侧---左左(指cur和新增结点和parent的相对位置):右单旋
抽象图:
具体图:
当子树高度为0时
当子树高度为1时
对以上图示进行分析,触发右旋的条件是:当parent为-2 ,cur为-1时触发右旋
右旋的核心代码:
解释:将cur的右边子树结点放到parent的左边,然后将parent放到cur的右边
伪代码:
parent->_left = curright;
cur->_right = parent;
2.新节点插入较高右子树的右侧---右右:左单旋
抽象图:
具体图:
当子树高度为0时
当子树高度为1时
对以上图示进行分析,触发左旋的条件是:当parent为2 ,cur为1时触发右旋
左旋的核心代码:
解释:将cur的左边子树结点放到parent的右边,然后将parent放到cur的左边
伪代码:
parent->_right = curleft;
cur->_left = parent;
3. 新节点插入较高左子树的右侧---左右:先左单旋再右单旋
以下图示,新增结点之前是符合AVL树规则的子树
抽象图:
1
具体图:
当子树高度为0时,新增结点就是60
curright为0的情况
当子树高度为1,且新增结点在curright左边
当子树高度为1,且新增结点在curright右边
左右双旋:当parent 的_bf为-2 cur的_bf为1时 触发左右双旋
左右双旋操作:1. 以30为旋转点进行左旋 2.以90为旋转点进行右旋 3.调整父亲指针的指向 4.调整平衡因子
4. 新节点插入较高右子树的左侧---右左:先右单旋再左单旋
抽象图:
具体图:
当子树高度为0时,新增结点就是60
当子树高度为1时,且新增结点在curleft的左边
当子树高度为1时,且新增结点在curleft的右边
右左双旋:当parent 的_bf为2 cur的_bf为-1时 触发右左双旋
右左双旋操作:1. 以90为旋转点进行右旋 2.以30为旋转点进行左旋 3.调整父亲指针的指向 4.调整平衡因子
对AVL树新增结点进行总结:
1.新增结点在左,_bf--
2.新增结点在右,_bf++
3.更新后_bf == 0,,parent所在子树高度不变不会影响到,不会影响祖先,这时不用沿着_root的路径往上更新,这时插入结束
4.更新后_bf == 1/-1,parent所在子树高度不变,不会影响祖先,这时就需要沿着_root的路径往上更新
5.更新后_bf == 2/-2,parent所在子树高度发生变化且不平衡,对parent所在子树进行旋转,使它平衡,平衡之后,插入结束
5->(1).当parent为-2 ,cur为-1时触发右旋
5->(2).当parent为2 ,cur为1时触发左旋
5->(3).当parent为-2 ,cur为1时触发左右双旋
当子树高度为0时
parent _bf: 0 cur _bf: 0 curleft _bf: 0
当子树高度为1,且新增结点在curright的左边
parent _bf: 1 cur _bf: 0 curleft _bf: 0
当子树高度为1,且新增结点在curright的右边
parent _bf: 0 cur _bf: -1 curleft _bf: 0
5->(4).当parent为2 ,cur为-1时触发右左双旋
当子树高度为0时
parent _bf: 0 cur _bf: 0 curleft _bf: 0
当子树高度为1,且新增结点在curleft的左边
parent _bf: 0 cur _bf: 1 curleft _bf: 0
当子树高度为1,且新增结点在curleft的右边
parent _bf: -1 cur _bf: 0 curleft _bf: 0
6.平衡因子一直更新到根结点,插入结束
AVL树insert的代码实现
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree()
:_root(nullptr)
{}
bool insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* cur = _root;
Node* parent = nullptr; //当cur为空树时下面循环都进不去所以给parent赋值cur就行了
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 > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
while (parent)
{
if (parent->_left == cur)
{
--parent->_bf;
}
else
{
++parent->_bf;
}
if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}
else if(parent->_bf == 2 || parent->_bf == -2)
{
//左旋转
if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent);
}
//右旋转
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
//右左双旋
else if(parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent);
}
//左右双旋
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent);
}
break;
}
else
{
assert(false);
}
}
return true;
}
//左旋转
void RotateL(Node* parent)
{
++Rotate_Count;
Node* cur = parent->_right;
Node* curleft = cur->_left;
parent->_right = curleft;
if (curleft)
{
curleft->_parent = parent;
}
cur->_left = parent;
Node* ppNode = parent->_parent;
parent->_parent = cur;
if (ppNode == nullptr)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = cur;
}
else
{
ppNode->_right = cur;
}
cur->_parent = ppNode;
}
parent->_bf = cur->_bf = 0;
}
//右旋转
void RotateR(Node* parent)
{
++Rotate_Count;
Node* cur = parent->_left;
Node* curright = cur->_right;
parent->_left = curright;
if (curright)
{
curright->_parent = parent;
}
cur->_right = parent;
Node* ppNode = parent->_parent;
parent->_parent = cur;
if (ppNode == nullptr)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = cur;
}
else
{
ppNode->_right = cur;
}
cur->_parent = ppNode;
}
parent->_bf = cur->_bf = 0;
}
//左右双旋
void RotateLR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
//保存curright的平衡因子,用来判断新增结点情况
int bf = curright->_bf;
RotateL(parent->_left);
RotateR(parent);
/*调整双旋之后的平衡因子
三种情况
1. 当curright平衡因子为0时 curright新增结点为本身
2. 当curright平衡因子为-1时 curright的左边插入新节点
3. 当curright平衡因子为1时 curright的右边插入新结点*/
if (bf == 0)
{
parent->_bf = 0;
cur->_bf = 0;
curright->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
cur->_bf = 0;
curright->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
cur->_bf = -1;
curright->_bf = 0;
}
else
{
assert(false);
}
}
//右左双旋
void RotateRL(Node* parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
//保存curleft的平衡因子,用来判断新增结点情况
int bf = curleft->_bf;
RotateR(parent->_right);
RotateL(parent);
//调整双旋之后的平衡因子
//三种情况
//1. 当curleft平衡因子为0时 curleft新增结点为本身
//2. 当curleft平衡因子为-1时 curleft的左边插入新节点
//3. 当curleft平衡因子为1时 curleft的右边插入新结点
if (bf == 0)
{
parent->_bf = 0;
cur->_bf = 0;
curleft->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
cur->_bf = 1;
curleft->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
cur->_bf = 0;
curleft->_bf = 0;
}
else
{
assert(false);
}
}
//验证平衡因子
bool IsBalance()
{
return IsBalance(_root);
}
int TreeHight()
{
return TreeHight(_root);
}
private:
Node* _root;
public:
int Rotate_Count = 0;
};
AVL树的平衡验证
虽然我们可以通过中序遍历来判断该树是否有序,但这只是二叉搜索树的性质,平衡二叉搜索树则需要判断它的平衡因子,我们可以通过右子树 - 左子树之差与根结点的平衡因子对比判断这颗树是否是一颗平衡二叉树,如果返回的为1则该树是一颗平衡树,否则,平衡因子出现异常
int TreeHight(Node* root)
{
if (root == nullptr)
{
return 0;
}
int lefthight = TreeHight(root->_left);
int righthight = TreeHight(root->_right);
//改错:注意不要返回时left的子树对上right的子树,这样会导致低的那一方高度加1
return lefthight > righthight
? lefthight + 1
: righthight + 1;
}
bool IsBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int lefthight = TreeHight(root->_left);
int righthight = TreeHight(root->_right);
if ((righthight - lefthight) != root->_bf)
{
cout << "平衡因子异常: " << root->_kv.first << " " << righthight - lefthight<< "->" << root->_bf << endl;
return false;
}
//检查平衡因子,其中一个有异常就返回false
//这里需要加绝对值,如果差是-2就不好判断了
return abs(righthight - lefthight) < 2
&& IsBalance(root->_left)
&& IsBalance(root->_right);
}
AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这 样可以保证查询时高效的时间复杂度,即logN。但是如果要对AVL树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。