一、AVL树的定义
AVL树是最先发明的自平衡⼆叉搜索树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的
左右子树都是二叉搜索树,且左右子树的高度差的绝对值不超过1。AVL树是⼀颗高度平衡搜索⼆叉树,通过控制高度差去控制平衡。
思考⼀下为什么AVL树是高度平衡搜索⼆叉树,要求高度差不超过1,而不是高度差是0呢?0不是更好的平衡吗?我们先看下面的图:
画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是0的。比如图中的⼀棵树是2个结点,4个结点等情况下,高度差最好就是1,达不到高度差为0。
AVL树得名于它的发明者G.M.Adelson-Velsky和E.M.Landis,他们是两个前苏联的科学家,他们在1962年的论文《An algorithm for the organization of information》中发表了它。
AVL树整体结点数量和分布和完全⼆叉树类似,高度可以控制在 ,那么增删查改的效率也可
以控制在 ,相比⼆叉搜索树有了本质的提升。
二、AVL树的实现
1、平衡因子
关于AVL树的实现,这里我们引入一个平衡因子(balance factor)的概念,每个结点都有⼀个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度,也就是说任何结点的平衡因子等于0/1/-1,AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像⼀个风向标⼀样。
注:平衡因子不是实现AVL树的必备条件,有些地方没有使用平衡因子也能实现AVL树。
下图是含有平衡因子的AVL树:
对于8这个结点来说,它的左子树高度为3,右子树高度为2,那么它的平衡因子就是-1(右子树的高度减左子树的高度);对于1这个结点来说,它的左子树高度为0,右子树的高度也为0,所以它的平衡因子就是0。如果每个结点的平衡因子大于1或者小于-1,那么这棵树就不是AVL树了,它的平衡性遭到了破坏。 (AVL树相较于二叉搜索树最主要的特点就是平衡,若失去平衡增删查改的时间复杂度就无从谈起)
因为AVL是搜索二叉树,它的插入遵循搜索二叉树的规则,若插入13,如下图所示:
此时,10这个结点的平衡因子更新为2,那么就认为这个树不平衡它就不是AVL树了。针对插入前是AVL树,而插入后不是AVL树的情况,我们会对这棵树进行旋转使它继续保持平衡,旋转的概念下面会详细解释。
2、实现框架
template <class K,class V>
struct AVLTreeNode
{
pair<K, V> _kv; //结点的每个元素类型是pair,搜索场景是key/value
int _bf; //记录平衡因子,balance factor
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent; //需要父结点的指针,方便更新平衡因子
//结点的构造函数
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0) //新结点的平衡因子都是0(因为它的左右子树均为空树,高度都是0,相减也为0)
{}
};
template <class K,class V>
class AVLTree
{
typedef AVLNode<K, V> Node;
public:
//... 实现的主要功能写在这个部分
private:
Node* _root = nullptr; //只需记录树的根节点
};
3、插入函数(insert)
AVL树的插入规则和二叉搜索树一模一样,只不过它要单独更新结点中的parent指针,以及平衡因子。AVL树插入的大致过程如下:
- 按⼆叉搜索树规则插入一个值。
- 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点->根结点路径上结点的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。
- 更新平衡因子过程中没有出现问题,则插入结束。
- 更新平衡因子过程中出现不平衡,需要对不平衡子树旋转,旋转后调整平衡的同时,本质降低了子树的高度,这个更新过程不会影响上⼀层,所以插入结束。
平衡因子的更新:
更新原则:
- 平衡因子 = 右子树高度 - 左子树高度(也可以是左子树高度 - 右子树高度,那么逻辑就需要改变)
- 只有子树高度变化才会影响当前结点平衡因子
- 插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因子++,新增结点在
parent的左子树,parent平衡因子--(右-左) - parent所在子树的高度是否变化决定了是否会继续往上更新
更新停止条件(分为3种情况):
- 更新后parent的平衡因子等于0,更新中parent的平衡因子变化为-1->0或者1->0,说明更新前parent子树⼀边高⼀边低,新增的结点插入在低的那边,插入后parent所在的子树高度不变,不会影响parent的父结点的平衡因子,更新结束。
- 更新后parent的平衡因子等于1或-1,更新中parent的平衡因子变化为0->1或者0->-1(不可能是2->1或者-2->-1,因为插入前这棵树必须是AVL树),说明更新前parent子树两边⼀样高,新增的插入结点后,parent所在的子树⼀边高⼀边低,parent所在的子树符合平衡要求,但是高度增加了1,这会影响parent的父结点的平衡因子,所以要继续向上更新平衡因子,最坏情况下更新到根结点,更新结束。
- 更新后parent的平衡因子等于2或-2,更新中parent的平衡因子变化为1->2或者-1->-2,说
明更新前parent子树⼀边高⼀边低,新增的插入结点在高的那边,parent所在的子树高的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:1、把parent子树旋转平衡。2、降低parent子树的高度,恢复到插⼊结点以前的高度。所以旋转后也不需要继续往上更新(插入前后高度不变:插入前高度是h,插入后,经过旋转,高度还是h),更新结束。
以上3种情况都是针对插入前是AVL树,插入后也是AVL树的情景,第三种情况插入后不是AVL树,那我们就需要单独处理,进行旋转。
图例说明:
插入结点13,结点14的平衡因子由0变为-1,需要向上更新,更新到10结点,它的平衡因子由1变为2,此时破坏了平衡,10所在的子树已经不平衡,需要旋转处理。
插入结点-1,结点1的平衡因子由0变为-1,需要向上更新,更新到中间结点3,它的平衡因子由1变为0,但以3为根的子树高度不变,不会影响上⼀层,更新结束。
插入结点7,结点6的平衡因子由0变为1,需要向上更新,更新到8这个根结点,这是最坏情况。
在左子树中插入结点,右子树所有结点的平衡因子都不需要改变;在右子树中插入结点,左子树所有结点的平衡因子都不需要改变。
了解完上面的知识,那么代码就很好理解了:
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv); //根结点的_parent指针为nullptr
return true;
}
Node* parent = nullptr; //注意这里的parent是结点的父结点,不是结点中指向父结点的指针
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 < kv.first) //比较的是key值,value值不参与比较
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
//链接父亲
cur->_parent = parent;
//插入结点结束后还要控制平衡
//更新平衡因子,沿着cur和parent向上更新
while (parent) //保证parent不为空,如果parent为空就更新结束了,此时cur就是根结点
{
if (cur == parent->_left) //parent的平衡因子需要--
parent->_bf--;
else //parent的平衡因子需要++
parent->_bf++;
//对应更新停止的第一种情况
if (parent->_bf == 0)
{
break;
}
//对应更新停止的第二种情况
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent =