平衡二叉查找树——AVL树
二叉查找树在最坏情况下高度可能为N-1,即插入元素时后插入的元素总比以前插入的元素大或者小。为了解决这种不平衡的情况,引入了平衡条件来限制树中节点的深度不能过深,其中最老的一种平衡树称为AVL树。这种树限制树中每个节点的左右子树的高度相差不能超过一。(另一种更严格的树限制节点的左右子树高度必须相等,但这样的树要求树中的节点数目为2的k次幂减1,是一种理想平衡树,但是要求太严格,无法实际使用。)
AVL树平衡条件分析
AVL树是一棵特殊的二叉查找树,对AVL树的操作中,除了插入操作与普通二叉查找树不同外,其他对AVL树的操作与对普通二叉查找树的操作相同。AVL树的插入操作与普通二叉查找树的不同之处在于它在插入新元素(这个操作就是普通的二叉查找树插入操作)后需要判断新树是否满足AVL树的平衡条件,如果不满足平衡条件需要对新树进行调整使其满足平衡条件。进行调整的起点就在第一个(即最深的节点)破坏平衡条件的节点处。可以证明,只要调整完以此节点为根的子树使其满足平衡树的平衡条件,就可以保证整棵树满足平衡树的平衡条件。
当插入新元素后,可能造成新树不满足平衡条件的情况可以分为四种。设a为插入新元素后不满足平衡条件的第一个节点(即不满足左右子树高度差不大于1的最深节点),在此基础上描述这四种情况:
(1) 对a的左子树的左子树进行一次插入
(2) 对a的左子树的右子树进行一次插入
(3) 对a的右子树的左子树进行一次插入
(4) 对a的右子树的右子树进行一次插入
其中情况(1)和(4)在逻辑上是同一种情况,(2)和(3)是同一种情况。但是在编程上需要进行不同的处理(思路相同)。其中对(1)和(4)使用单旋转,对(2)和(3)使用双旋转(即两次单旋转)处理。旋转的主要思路就是通过降低a的深度,使其作为原来以a为根的树中的某个节点(具体情况选择的新根节点不同)的子节点,从而降低原来以a为根的子树中较高的那棵子树的深度,提高原来较低的子树的深度,从而使其满足平衡树的平衡条件。
AVL树基本例程
AVL树节点的基本结构与普通的二叉查找树节点结构没有太大区别,唯一的区别是多了一个高度信息:
struct AvlNode
{
ElementType Element;
AVLTree Left;
AVLTree Right;
int Height;
} ;
另外相应的,AVL树有一个计算获取节点高度的例程,如下:
static int Height(Position P)
{
if(P==NULL)
return -1;
else
return P->Height;
}
单旋转
单旋转处理情形(1)和(4)的情况,单旋转的主要思路就是将第一个不平衡节点的左子树(或者右子树,取决于左或者右单旋。情形(1)是左单旋,情形(4)是右单旋)的根节点作为不平衡树的新根节点,原来的根节点作为新根节点的右子树(或者左子树)的根节点。同时新根节点的原来的右子树(或者左子树)作为原来不平衡树根节点的左子树(或者右子树)。最后,需要考虑将不平衡树的新根节点返回给上一层作为上一层节点的某一棵子树的根,这部分工作是由插入例程的递归机制处理的。例程如下:
//K2是一个不平衡树,同时是第一个不平衡节点
static Position SingleRotateWithLeft(Position K2)
{
Postion K1;
K1=K2->Left;
K1->Right=K2;
K2->Left=K1->Right;
K2->Height=Max(Height(K2->Left),Height(K2->Right))+1;
K1->Height=Max(Height(K1->Left),K2->Height)+1;
Return K1;
}
双旋转
双旋转处理(2)和(3)的情况,此时使用一次单旋转无法降低高度较高的子树的深度,原因是较高的那棵子树会作为被降低了深度的原不平衡树的根的子树,此时与未旋转前的树深度相同。解决方案是使用两次单旋转,首先对不平衡节点的左(右)子树进行一次右(左)单旋转,使高子树(不平衡节点的左(右)子树的右(左)子树)的根向上提,降低高子树的深度。然后再对不平衡节点应用一次左(右)单旋转,将经历一次单旋转后成为不平衡节点的子树的根的节点继续向上提升,这样两次旋转后,就可以使不平衡树重新成为平衡树。例程如下:
//K3是第一个不平衡节点
static Postion DoubleRotateWithLeft(Position K3)
{
K3->Left=SingelRotateWithRight(K3->Left);
Return SingleRotateWithLeft(K3);
}
插入操作
AVL树保持平衡是在插入时保证的,每当插入一个新节点时,都会判断树是否保持平衡状态,如果不平衡则对第一个不平衡节点进行旋转处理,插入例程根据插入新节点的情况来选择具体进行的旋转的方式。简单来说,AVL树的插入操作是在普通二叉查找树插入操作基础上进行了两个操作,分别是判断是否满足平衡条件的操作以及在不满足平衡条件的情况下进行旋转的操作。例程如下:
AVLTree Insert(ElementType X,AVLTree T)
{
if(T==NULL)
{
T=malloc(sizeof(struct AvlNode));
If(T==NULL)
FataError(“Out OF space!!!”);
Else
{
T->Element=X;
T->Height=0;
T->Left=T->Right=NULL;
}
}
else if(x<T->element)
{
T->Left=Insert(X,T->Left);
//保持平衡的操作
If(Height(T->left)-Height(T->Right)==2)
{
if(X<T->Left->Element)
T=SingleRotateWithLeft(T);
Else
T=DoubleRotateWihtRight(T);
}
}
else if(X>E->element)
{ //与插入左子树时相反的操作… }
T->Heihgt=Max(Height(T->Left),Height(T->Rigth))+1;
Return T;
}