在说平衡树之前我们得先复习一下二叉搜索树BST的定义:
显然我们如果有一个已经建立好二叉搜索树的序列,那就可以很容易地找出某个数的前驱、排名(或者求第k大的数)等,时间复杂度与树的高度有关,一般为 O ( l o g 2 n ) O(log_2n) O(log2n)
不过,参考下列的序列,如果建立二叉搜索树,则收效甚微:
1 , 2 , 4 , 5 , 7 , 9 , 3 , 10 , 14 , 13 , 17 1, 2, 4, 5, 7, 9, 3, 10, 14, 13, 17 1,2,4,5,7,9,3,10,14,13,17
这一序列大部分是有序递增的,这就导致我们总是插入右子树,也就使得二叉树变成了“蚯蚓形”,高度大大增加。进而时间复杂度也接近 O ( n ) O(n) O(n),失去了树结构的优势
平衡树要实现的特性比较直接:让每棵二叉搜索树的左右子树高度相差不大,这样就能保持住 O ( l o g 2 n ) O(log_2n) O(log2n) 的时间优势,AVL算法是实现途径之一
建立一棵AVL树需要在二叉搜索树BST每个节点上加入 平衡因子 这一概念:
记录平衡因子的过程很简单,只需要在插入的时候对经过的父节点进行更新即可
不过我们并不会让这一数字的绝对值大于等于2,因为每次插入之后我们会回溯,如果检查到某一节点的平衡因子绝对值大于等于2,则对此节点进行旋转操作。进而将平衡因子绝对值控制到小于等于1
如何旋转在下面介绍
先表明一下我们在这棵AVL树中用到的变量:
struct avl
{
int fa; //父节点
int ls; //左儿子
int rs; //右儿子
int v; //节点权值
int bt; //平衡因子
}
可知,我们旋转的时候,有可能是bt <= -2或者bt >= 2(即左子树偏高与右子树偏高),之后便涉及到四种旋转:LL,RR,LR,RL,先介绍简单情况下的前两种
这是最为简单的LL旋转
较为完整的表述:对某一节点进行LL旋转,就是让他的左儿子替代它的位置,它成为左儿子的右儿子,然后左儿子的右儿子成为它的左儿子。 下图涵盖了这一情况
完整地实践了上述加粗的表述
实现函数如下
void ll(int o)
{
int oo = aa[o].ls;
aa[oo].fa = aa[o].fa;
if (aa[oo].fa == 0)
{
ro = oo;
}
if (aa[o].fa)
{
if (aa[aa[o].fa].v < aa[o].v)
{
aa[aa[o].fa].rs = oo;
}
else
{
aa[aa[o].fa].ls = oo;
}
}
aa[o].fa = oo;
aa[o].ls = aa[oo].rs;
if (aa[oo].rs)
{
aa[aa[oo].rs].fa = o;
}
aa[oo].rs = o;
}
这里要说的是,如果理解了LL旋转,则RR旋转也就没有问题了,因为它就是LL旋转的镜像操作: