概述
二叉搜索树查找的最高效率类似于折半查找,但却十分依赖于树建立的顺序,如果一颗二叉搜索树的倾斜的,那么它的效率和顺序查找无二区别。只有左右平衡的二叉搜索树才能达到 O ( log n ) O(\log n) O(logn)的查找效率。
二叉搜索树的倾斜又称为不平衡问题,为了解决由于建立顺序造成二叉树倾斜,因此提出了平衡二叉树技术。这种结构与二叉搜索树主要的不同在于它可以通过自我的调整来实现平衡,而不在意是什么样的建树顺序。
本文假设你了解基本的二叉搜索树,如果不了解则可以查看此文:树与二叉树。
性质及定义
平衡二叉树(Balanced Binary Tree / Height-Balanced Tree),也叫做AVL树(来自于其发明者的名字)。
它具有如下性质:
- 它的左子树和右子树均为AVL树。
- AVL树的左子树与右子树的高度(深度)差绝对值不超过1。
AVL树的最大高度
由于第一条性质,因此一棵AVL树的高度在最坏的情况下的高度为: N h = N h − 1 + N h − 2 + 1 N_h = N_{h-1} + N_{h-2} +1 Nh=Nh−1+Nh−2+1,且有 N 0 = 0 , N 1 = 1 N_0 = 0, N_1 = 1 N0=0,N1=1。这个定义和斐波那契数列是高度相似的。
也就是说,若树有 n n n个结点,那么最大高度约为: l o g n log n logn
AVL树的存储结构
一般AVL树与二叉搜索树类似,均用链表实现,就不在这方面细谈。为了方便维护,使树保持平衡,一般会为每个结点加入平衡因子(BF, Balance Factor)。
对于结点 x x x而言,其平衡因子定义为: x x x的左子树高度减去其右子树的高度(也可以是右子树减去左子树,总之表现的是左右子树的高度差即可)。
从AVL树的第一条性质可知,平衡因子的取值只有1,0,-1
三种取值。如下图:
AVL树的插入
AVL树与二叉搜索树最大的不同就在于,AVL树在插入时会自动调整,使的树的 ∣ B F ∣ |BF| ∣BF∣值总是小于1。关键就在于如何实现这个过程,将插入后导致树的失衡调整为平衡状态。而这个调整过程是通过旋转实现的。
AVL树的旋转
一棵AVL树会因为插入一个新结点导致失衡,总的来说可以归咎为4类原因:
- 向某结点的左子树插入一个左子结点。
- 向某结点的左子树插入一个右子结点。
- 向某结点的右子树插入一个左子结点。
- 向某结点的右子树插入一个右子结点。
如下图:
上面的4种失衡状态的处理方式分为两类:单旋和双旋,前者又分为左旋(LL)和右旋(RR),后者则分为先左旋后右旋(LR)和先右旋后左旋(RL)。
由于失衡总是新结点插入所带来的,因此插入结点后就需要从插入位置回溯,检查一路上的结点是否仍处于平衡。若发现失衡点则从失衡结点起逆回溯路径走两层。这时候会发现情况可能有两种:失衡结点及回溯两层结点共计3个结点之间呈一直线(即为同一侧,如均为左子树、右子树)则需要单旋平衡;不能成一直线则需要双旋。
下面看看每一种的旋转到底是怎么回事。
单旋(LL/RR Rotate)
为了方便理解,先使用一个实际的实例来演示过程,如有一棵AVL树插入一个新结点10后如下图:
从结点10回溯至结点50时发现BF因子到了2,因此树已经失衡,从失衡处逆回溯两个节点(红色连线)形成一个直线。像这种向某子节点的左子树插入一个左子结点导致的失衡,则可以应用右单旋来调整平衡。
所谓的右单旋,即将形成直线的三个结点以中间(即结点25)为轴,将失衡结点向右旋转。为了更直观,把这个过程单独抽出来如下图所示:
这样一来,就实现了平衡的过程。
但有思考的读者就会发现,原轴结点25变为了新的父结点,原来的父结点则变为了新父结点的右子树,仍有原轴结点的右子树需要处理。但简单思考一番就可以得知,只需要将结点25的原右子树接于现结点50的左子树即可。完成后如下图:
另外要说明的是,为什么只需要简单的将原轴结点的右子树接于现右子树的左子树处可以通过以下方式说明:
首先,失衡必然是使ALV树的平衡因子由1变为2的过程。其次旋转利用的是发现失衡的结点及逆回溯路径上的两个连续结点实现的,而回溯路径上的其他结点一定仍然是平衡的(否则失衡结点就会发生变化)。因为这一点,在这种情况下,轴结点的右子树的高度与失衡结点的右子树高度应是相当的,否则则需要另一种旋转。
许多教科书喜欢用方块将子树的高度看做一个整体来描述这一特性,如下图:
上图应该很直观的就能表现出为什么轴结点的右子树高度与失衡结点右子树是相当的,否则需要的旋转就不是右单旋。
对于左单旋,则是右单旋的镜像,原理是一样的,稍加思考就能理解,就不在此浪费篇幅了。
双旋(LR/RL Rotate)
上面提到,如果失衡结点及逆回溯过程的两个连续结点能形成直线,只需要简单的应用单旋就可以了。但如果不能形成直线,则需要应用双旋,这个过程比单旋稍微复杂一些,但也很好理解。
同样举个实际例子,如给一颗AVL树增加新结点45后如下图:
失衡结点与逆回溯过程的连续两个结点形成的就不是直线了,这种情况就要先左单旋,但与普通左单旋不一样的是,这里的左单旋是以逆回溯连续结点的最后一个结点(即结点35)为轴,将另一个连续结点(即结点25)向左旋转,将这个过程单独抽出来如下图:
与普通左单旋一样,也需要将此时轴结点的左子树接于被旋转结点的右子树处,左旋完成后整体的结果如下图:
这样一来得到了一个很熟悉的中间结果,就是原本的"<"结构的节点组变为了直线,也就是说,此时只需要一个普通的右单旋,以中间为轴,将失衡结点向右旋转(与普通右单旋一样,需要将现作为轴的结点35的右子树接于被旋转结点的左子树)。结果如下图:
双旋其实就是将不是直线的情况先通过一个单旋转为直线,然后再通过另一个单旋使AVL树恢复平衡。RL双旋是LR双旋的镜像,同上就不再浪费篇幅细谈了。
AVL树的删除
删除是比插入复杂的多的操作,这里只简单提一下,不作实现。
若待删除结点的两个子树均非空,则用该结点的左子树中序遍历的最后一个节点(或右子树的第一个结点)替换该结点,将情况转为待删除结点只剩一个子树再处理。
当待删除结点只有一个子树的情况,则应该更新从被删除结点到树根路上所有的平衡因子,对于路径上的每一个失衡结点都需要做平衡处理。
C语言实现
typedef int ElementType;
typedef struct AVLNode *Position;
typedef Position AVLTree; /* AVL树类型 */
typedef struct AVLNode {
ElementType Data; /* 结点数据 */
AVLTree Left; /* 指向左子树 */
AVLTree Right; /* 指向右子树 */
int Height; /* 树高(平衡因子) */
}AVLNode;
int Max( int a, int b )
{
return a > b ? a : b;
}
int GetHeight( AVLTree p )
{
return p == NULL ? 0 : p->Height;
}
AVLTree SingleRightRotation( AVLTree A )
{
/* 将A与B做右单旋,更新A与B的高度,返回新的根结点B */
AVLTree B = A->Right;
A->Right = B->Left;
B->Left = A;
A->Height = Max( GetHeight( A->Left ), GetHeight( A->Right ) ) + 1;
B->Height = Max( GetHeight( B->Right ), A->Height ) + 1;
return B;
}
AVLTree SingleLeftRotation( AVLTree A )
{
/* 将A与B做左单旋,更新A与B的高度,返回新的根结点B */
AVLTree B = A->Left;
A->Left = B->Right;
B->Right = A;
A->Height = Max( GetHeight( A->Left ), GetHeight( A->Right ) ) + 1;
B->Height = Max( GetHeight( B->Left ), A->Height ) + 1;
return B;
}
AVLTree DoubleLeftRightRotation( AVLTree A )
{
/* 将B与C做右单旋,C被返回 */
A->Left = SingleRightRotation( A->Left );
/* 将A与C做左单旋,C被返回 */
return SingleLeftRotation( A );
}
AVLTree DoubleRightLeftRotation( AVLTree A )
{
/* 将B与C做左单旋,C被返回 */
A->Right = SingleLeftRotation( A->Right );
/* 将A与C做右单旋,C被返回 */
return SingleRightRotation( A );
}
AVLTree Insert( AVLTree T, ElementType X )
{ /* 将X插入AVL树T中,并且返回调整后的AVL树 */
if ( !T ) { /* 若插入空树,则新建包含一个结点的树 */
T = (AVLTree)malloc( sizeof( struct AVLNode ) );
T->Data = X;
T->Height = 0;
T->Left = T->Right = NULL;
}
else if ( X < T->Data ) {
/* 插入T的左子树 */
T->Left = Insert( T->Left, X );
/* 如果需要左旋 */
if ( GetHeight( T->Left ) - GetHeight( T->Right ) == 2 )
if ( X < T->Left->Data )
T = SingleLeftRotation( T ); /* 左单旋 */
else
T = DoubleLeftRightRotation( T ); /* 左-右双旋 */
}
else if ( X > T->Data ) {
/* 插入T的右子树 */
T->Right = Insert( T->Right, X );
/* 如果需要右旋 */
if ( GetHeight( T->Left ) - GetHeight( T->Right ) == -2 )
if ( X > T->Right->Data )
T = SingleRightRotation( T ); /* 右单旋 */
else
T = DoubleRightLeftRotation( T ); /* 右-左双旋 */
}
/* else X == T->Data,无须插入 */
/* 别忘了更新树高 */
T->Height = Max( GetHeight( T->Left ), GetHeight( T->Right ) ) + 1;
return T;
}