数据结构与算法16—平衡二叉(AVL)树
我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度O(log2n)同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。于是就有了我们下边介绍的平衡二叉树。
平衡二叉树
定义:
平衡二叉树或者是空树,或者是满足下列性质的二叉树。
⑴:左子树和右子树深度之差的绝对值不大于1;
⑵:左子树和右子树也都是平衡二叉树。
平衡因子(Balance Factor) :二叉树上结点的左子树的深度减去其右子树深度称为该结点的平衡因子。
因此,平衡二叉树上每个结点的平衡因子只可能是-1、0和1,即
|hl−hr|≤1。 否则,只要有一个结点的平衡因子的绝对值大于1, 该二叉树就不是平衡二叉树。
最小二叉平衡树的节点的公式如下:F(n)=F(n−1)+F(n−2)+1
这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n−1)是左子树的节点数量,F(n−2)是右子树的节点数量。
平衡查找树(AVL树)
二叉排序树是一种查找效率比较高的组织形式,但其平均查找长度受树的形态影响较大,形态比较均匀时查找效率很好,形态明显偏向某一方向时其效率就大大降低。因此,希望有更好的二叉排序树,其形态总是均衡的,查找时能得到最好的效率,这就是平衡二叉排序树。
平衡二叉排序树(Balanced Binary Tree或Height-Balanced Tree)是在1962年由Adelson-Velskii和Landis提出的,一种高度平衡的排序二叉树,其每一个节点的左子树和右子树的高度差最多等于1,又称AVL树。
如果一棵二叉树既是二叉排序树又是平衡二叉树,称为平衡二叉(查找)排序树(Balanced Binary Sort Tree) 。
平衡二叉树所有包括分支节点和叶节点的平衡因子只可能是-1,0和1,只要有一个节点的因子不在这三个值之内,该二叉树就是不平衡的。
纠错:图1是不平衡的
最小不平衡子树:距离插入结点最近的,且平衡因子的绝对值大于1的节点为根的子树。
n个结点的AVL树最大深度约1.44log2n。查找、插入和删除在平均和最坏情况下都是O(logn)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
可以采用动态平衡技术保持一个平衡二叉树。构造平衡二叉树的时候,也可以采用相同的方法,默认初始时,是一个空树,插入节点时,通过动态平衡技术对二叉树进行调整。
Adeleon-Velskii和Landis提出了一个动态地保持二叉排序树平衡的方法,其基本思想是:在构造二叉排序树的过程中,每当插入一个结点时,首先检查是否因插入而破坏了树的平衡性,若是因插入结点而破坏了树的平衡性,则找出其中最小不平衡树,在保持排序树特性的前提下,调整最小不平衡子树各结点之间的连接关系,以达到新的平衡。通常这样得到的平衡二叉排序树简称为AVL树。
AVL树的自平衡操作——旋转
AVL树最关键的也是最难的一步操作就是旋转。旋转主要是为了实现AVL树在实施了插入和删除操作以后,树重新回到平衡的方法。下面我们重点研究一下AVL树的旋转。
不平衡的四种情况
对于一个平衡的节点,由于任意节点最多有两个儿子,因此高度不平衡时,此节点的两颗子树的高度差2。容易看出,这种不平衡出现在下面四种情况:
- 1) 6节点的左子树3节点高度比右子树7节点大2,左子树3节点的左子树1节点高度大于右子树4节点,这种情况成为左左(LL)(左孩子的左子树深度大)。
- 2) 6节点的左子树2节点高度比右子树7节点大2,左子树2节点的左子树1节点高度小于右子树4节点,这种情况成为左右(LR)。
- 3) 2节点的左子树1节点高度比右子树5节点小2,右子树5节点的左子树3节点高度大于右子树6节点,这种情况成为右左(RL)。
- 4) 2节点的左子树1节点高度比右子树4节点小2,右子树4节点的左子树3节点高度小于右子树6节点,这种情况成为右右(RR)。
从图2中可以可以看出,1和4两种情况是对称的,这两种情况的旋转算法是一致的,只需要经过一次旋转就可以达到目标,我们称之为单旋转。2和3两种情况也是对称的,这两种情况的旋转算法也是一致的,需要进行两次旋转,我们称之为双旋转。
单旋转
单旋转是针对于左左(LL)和右右(RR)这两种情况的解决方案,这两种情况是对称的,只要解决了左左这种情况,右右就很好办了。图3是左左情况的解决方案,节点k2不满足平衡特性,因为它的左子树k1比右子树Z深2层,而且k1子树中,更深的一层的是k1的左子树X子树,所以属于左左情况。
为使树恢复平衡,我们把k1变成这棵树的根节点,因为k2大于k1,把k2置于k1的右子树上,而原本在k1右子树的Y大于k1,小于k2,就把Y置于k2的左子树上,这样既满足了二叉查找树的性质,又满足了平衡二叉树的性质。
这样的操作只需要一部分指针改变,结果我们得到另外一颗二叉查找树,它是一棵AVL树,因为X向上一移动了一层,Y还停留在原来的层面上,Z向下移动了一层。整棵树的新高度和之前没有在左子树上插入的高度相同,插入操作使得X高度长高了。因此,由于这颗子树高度没有变化,所以通往根节点的路径就不需要继续旋转了。
同理右右即是向左转。
双旋转
双旋转:对于左右和右左这两种情况,单旋转不能使它达到一个平衡状态,要经过两次旋转。双旋转是针对于这两种情况的解决方案,同样的,这样两种情况也是对称的,只要解决了左右这种情况,右左就很好办了。图4是左右情况的解决方案,节点k3不满足平衡特性,因为它的左子树k1比右子树Z深2层,而且k1子树中,更深的一层的是k1的右子树k2子树,所以属于左右情况。
为使树恢复平衡,我们需要进行两步,第一步,把k1作为根,进行一次右右旋转,旋转之后就变成了左左情况,所以第二步再进行一次左左旋转,最后得到了一棵以k2为根的平衡二叉树树。
小结:
左左或右右:向右转 或 向左转
左右或右左:失衡左孩向左转,首失衡节点向右转 或 失衡右孩向右转,首失衡节点向左转
构建过程
设要构造的平衡二叉树中各结点的值分别是(1,2,3,4,5,6,7,10,9),平衡二叉树的构造过程如图
关键代码示例
/*平衡二叉树的定义结构*/ typedef strucr avlnode { datatype data; /*数据元素*/ int bf; /*平衡因子*/ struct avlnode * lchild, * rchild; /*右孩子*/ }AVLNode, * AVLTree; /*LL调整*/ void LL_rotate(AVLTree *p) { /*对*p为根的子树,作LL处理,处理之后,p指向为子树的新根*/ lp = *p->lchild; //lp指向*p左子树根结点 *p->lchild = lp->rchild; //lp的右子树挂接*p的左子树 lp->rchild = *p; *p->bf = 0; lp->bf = 0; //修改相关的平衡因子 *p = lp; //p指向新的根结点 } /*RR调整*/ void RR_rotate(AVLTree *p) { /*对*p为根的子树,作LL处理,处理之后,p指向为子树的新根*/ lp = *p->rchild; //lp指向*p右子树根结点 *p->rchild = lp->lchild; //lp的子左树挂接*p的右子树 lp->rchild = *p; *p->bf = 0; lp->bf = 0; //修改相关的平衡因子 *p = lp; //p指向新的根结点 } /*LR调整*/ void LR_rotate(AVLTree *p) { /*对*p为根的子树,作LL处理,处理之后,p指向为子树的新根*/ lp = *p->lchild; //lp指向失衡结点a的左孩子b rlp = lp->rchild; //rlp指向lp的右孩子 if(rlp->bf == -1) r=1; else r=0; //记忆是LR1还是LR2 RR_rotate(&(*p->lchild)); //对*p的左子树进行RR处理 LL_rotate(p); //对*p进行LL处理 if(r==1){ //改变相关结点的平衡因子 *p->bf = 0; lp->bf = 1; }else{ *p->bf = -1; lp->bf = 0; } *p = rlp; *p->bf = 0; } /*RL调整*/ void RL_rotate(AVLTree *p) { /*对*p为根的子树,作LL处理,处理之后,p指向为子树的新根*/ rp = *p->lchild; //lp指向失衡结点a的右孩子b lrp = rp->lchild; //lrp指向rp的左孩子 if(lrp->bf == -1) lh=1; else lh=0; //记忆是LR1还是LR2 LL_rotate(&(*p->rchild)); //对*p的右子树进行LL处理 RR_rotate(p); //对*p进行RR处理 if(lh==1){ //改变相关结点的平衡因子 *p->bf = 0; rp->bf = -1; }else{ *p->bf = 1; rp->bf = 0; } *p = lrp; *p->bf = 0; }