一.平衡二叉树的定义:
1.平衡二叉树简称平衡树(AVL树,该缩写来源于平衡二叉树的发明人的名字简称);
2.结点的平衡因子=左子树高-右子树高;
3.以上述图片左下角的二叉树为例,结点50的左子树的高度为2,右子树的高度为3,因此结点50的平衡因子为2-3即-1;
4.显然如果一棵二叉树是平衡的,那么各个结点的平衡因子只可能是-1、0、1,也就是绝对值不超过1,如果二叉树中有任何一个结点的平衡因子绝对值超过1,那么这棵二叉树一定不是平衡二叉树;
5.在平衡二叉树结点的结构体中可以新增一个参数int balance记录平衡因子,如下:
#include<stdio.h>
//平衡二叉树结点
typedef struct AVLNode
{int key; //结点的数据域,该变量的类型不固定,因题而异int balance; //平衡因子struct AVLNode *lchild,*rchild; //结点的左孩子指针、右孩子指针
}AVLNode,*AVLTree; //AVLNode代表平衡二叉树的结点,*AVLTree代表平衡二叉树
int main()
{return 0;
}
6.之前说过,如果使一棵二叉排序树保持平衡,就可以保证这棵二叉排序树的查找效率达到O(log₂n)即查找效率达到最好,因此要重点研究在插入一个新结点之后,如何保持二叉树的平衡。
二.平衡二叉树的插入:
实例:
以上述图片左边的二叉排序树为例,
现在插入关键字为67的结点,根据二叉排序树的特性可得知插入的具体位置在关键字为68的结点的左子树上,
新插入关键字为67的结点的所有祖先的平衡因子都受到了影响,
现在要让二叉排序树中每一个结点恢复平衡的方法就是可以从新插入的结点开始往上找,找到第一个不平衡的结点
->本例中从关键字为67的结点想上找第一个不平衡的结点是关键字为70的结点,该结点的平衡因子为2,它是距离新插入的结点67第一个不平衡的结点,现在可以把以关键字为70的结点作为根结点的子树称为"最小不平衡子树",之所以称为最小,是因为如果继续往上找,第二个不平衡的结点是关键字为66的结点,以这个结点为根结点的子树显然要更大,实际中只需要调整这棵"最小不平衡子树"即可。
调整的效果如下图:
具体的调整规则之后探讨,只需要知道把这棵"最小不平衡子树"恢复平衡之后,其他的祖先结点就都恢复了平衡,
所以当插入一个新结点之后导致不平衡的话,只需要调整"最小不平衡子树"就可以让其他结点恢复平衡。
三.调整"最小不平衡子树"->LL和RR:
分为四种情况,分别是LL、RR、LR、RL:
1.LL:在A结点的左孩子的左子树中插入新结点导致A结点不平衡->需要右旋
以上述图片为例,
-
灰色方形的框表示结点的子树,该子树的高度用H表示,其中这些子树都是平衡的
-
BL表示B结点的左子树,BR表示B结点的右子树,AR表示A结点的右子树
最左边的二叉树中A结点的左子树是以B结点为根结点的树,其中BL的高度为H,BR的高度也为H,算上B结点,根据二叉树的定义可知以B结点为根结点的树的高度为H+1(加的1是B结点的高度),A结点的右子树是AR,高度为H,
所以A结点的平衡因子=(H+1)-H=1,所以A结点是平衡的;B结点的平衡因子=H-H=0,所以B结点是平衡的;
此时在A结点的左孩子即B结点的左子树BL中插入一个新结点(上述图片中间的二叉树),会导致A结点不平衡,这是因为B结点的左子树BL变高了,BL的高度变为了H+1,所以A结点的左子树整体来看高度变为了H+2,而A结点的右子树AR的高度仍保持为H,所以此时A结点的平衡因子=(H+2)-H=2,导致A结点不平衡;B结点的平衡因子=(H+1)-H=1,所以B结点是平衡的,
(注:假定以A结点为根结点的二叉树就是最小不平衡子树)
因此需要调整以A结点为根结点的二叉树(即上述图片中最中间的二叉树)使其保持平衡,
如下图:
如上图,
要调整以A结点为根结点的二叉树即"最小不平衡子树"(即上述图片中最中间的二叉树)使其平衡,
调整的结果需要满足:
-
恢复"最小不平衡子树"的平衡
-
使得"最小不平衡子树"在调整之后依然满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"
在本例中结点的大小关系为BL<B<BR<A<AR(注:以A结点为根结点的二叉树是二叉排序树,在A结点的左子树中的每一个结点的值都小于结点A的值,比如BR<A,B<A),
使得以A结点为根结点的二叉树即"最小不平衡子树"恢复平衡的处理方法如下:
步骤一:让B结点右旋->具体的做法就是让B结点向右上旋转代替A结点成为该二叉树的根结点,B结点的右子树就是以A结点为根结点的树;
步骤二:处理子树BL、BR、AR->
-
首先看子树BL,BL之前是B结点的左子树,BL的值小于B结点值(BL<B),所以只能把BL连接在B结点的左子树,因为要保证"左子树结点值<根结点值<右子树结点值"
-
再看子树BR,BR之前是B结点的右子树,现在B结点的右子树已经挂了A结点,那么BR就不能作为B结点的右子树了,但是有BR的值小于A结点的值(BR<A),所以可以把子树BR当作A结点的左子树
-
最后看子树AR,AR之前是A结点的右子树,有AR的值大于A结点的值(A<AR),所以AR继续作为A结点的右子树
这样就可以既保证二叉排序树的特性,同时BL这个部分的高度是H+1即成功插入了新结点,
经过上述的调整之后(上述图片最右边的树)B结点的左子树BL的高度为H+1、右子树的高度为H+1,即B结点的平衡因子=(H+1)-(H+1)=0,
A结点的左子树BR的高度为H、右子树AR的高度为H,即A结点的平衡因子=H-H=0,
可知以A结点为根结点的子树和以B结点为根结点的子树都保持了平衡,
这样一来就可以让"最小不平衡子树"恢复平衡,同时又保持二叉排序树的特性。
Ⅰ.思考1:
为什么要假定所有灰色方形的框表示的子树的高度都是H呢,那有没有其他的情况?
首先看上述图片中最左边的树的AR,A结点的右子树AR假设高度为H,如果AR高度改为H+1,由于A结点的左子树的高度也为H+1,所以此时A结点的平衡因子=(H+1)-(H+1)=0,A结点是平衡的
这种情况下在BL插入一个新结点,BL的高度变为H+1,所以A结点左子树的高度为H+2,A结点的右子树的高度为H+1,那么A结点平衡因子=(H+2)-(H+1)=1,此时A结点是平衡的,就不会出现所需要探讨的不平衡的情况,所以A结点右子树的高度一定是H,而不是H+1。
Ⅱ.思考2:
为什么要假定所有灰色方形的框表示的子树的高度都是H呢,那有没有其他的情况?
首先看上述图片中最左边的树的AR,A结点的右子树AR假设高度为H,如果AR高度改为H-1,由于A结点的左子树的高度为H+1,所以此时A结点的平衡因子=(H+1)-(H-1)=2,使得A结点一开始就是不平衡的,而现在要探讨的是插入一个新结点后才导致A结点不平衡,与题目要求违背了。
Ⅲ.思考3:
为什么要假定所有灰色方形的框表示的子树的高度都是H呢,那有没有其他的情况?
首先看上述图片中最左边的树的BR,B结点的右子树BR假设高度为H,如果BR高度改为H-1,由于B结点的左子树BL的高度为H,所以此时B结点的平衡因子=H-(H-1)=1,可知B结点是平衡的,满足平衡二叉树的条件,
但是在这种情况下,如果B结点的左子树BL添加一个结点后高度变为H+1,这时B结点的平衡因子=(H+1)-(H-1)=2,导致B结点不平衡,这样的话B结点就是距离新插入的结点最近的不平衡点,所以以B结点为根结点的树就是"最小不平衡子树"了,但现在研究的是以A结点为根结点的树是"最小不平衡子树",所以B结点的右子树BR的高度不可能是H-1,而是H->其他的情况同理,自行推理。
总之,在这个地方只要假定某一个灰色方形的框表示的子树的高度是H的话,那其他的灰色方形的框表示的子树的高度也必须是H,只有这样才能在LL处插入新结点后导致A结点是为"最小不平衡子树"的根结点。
2.LL的代码实现:
以上述图片中的二叉排序树为例,
-
f指针(father的缩写)指向A结点即f指针表示A结点;A结点的左子树为f->lchild;A结点的右子树为f->rchild(注:f指针始终指向A结点,不会改变)
-
p指针指向B结点即p指针表示B结点;B结点的左子树为p->lchild;B结点的右子树为p->rchild(注:p指针始终指向B结点,不会改变)
-
gf指针(grandfather的缩写)指向整个二叉树的根结点的父结点;由于整个二叉树的根结点可能是其父结点的左孩子,也可能是右孩子,所以整个二叉树的根结点既可能是gf->lchild,也可能是gf->rchild,因此gf->lchild或gf->rchild就表示整个二叉树的根结点(注:整个二叉树的根结点未必是A结点,也可能是B结点)
根据之前的分析,要让LL的情况恢复平衡,需要B结点右旋,旋转的结果就是新的二叉树,其中->
-
根结点从A结点变为B结点
-
A结点的左子树从以B结点为根结点的树变为BR,A结点的右子树AR没变
-
B结点的左子树BL没变,B结点的右子树从BR变为以A结点为根结点的树
各个结点的子树情况 | 二叉树未"右旋"时 | 二叉树"右旋"后 |
---|---|---|
整棵二叉树的根结点即gf->lchild或gf->rchild | gf->lchild或gf->rchild的值为A结点即f | gf->lchild或gf->rchild的值为B结点即p |
A结点的左子树即f->lchild | f->lchild是以B结点为根结点的子树即p | f->lchild是子树BR |
A结点的右子树即f->rchild | f->rchild是子树AR | f->rchild是子树AR |
B结点的左子树即p->lchild | p->lchild是子树BL | p->lchild是子树BL |
B结点的右子树即p->rchild | p->rchild是子树BR | p->rchild是以A结点为根结点的子树即f |
对于代码的书写只需要针对发生改变的子树和结点即可,还需要注意其中的逻辑(代码不唯一):
步骤一:对于A结点,由于A结点只有左子树f->lchild发生了改变即从以B结点为根结点的子树p变为子树BR,所以可以是f->lchild = p->rchild;
步骤二:对于B结点,由于B结点只有右子树p->rchild发生了改变即从子树BR变为以A结点为根结点的子树f,所以可以是p->rchild = f;
步骤三:对于整棵二叉树,只有根结点发生了改变即从A结点f变为B结点p,所以可以是gf->lchild = p或gf->rchild = p;,
这样就可以实现"右旋"操作,同时可以保证这棵二叉树依然保持二叉排序树的特性。
3.RR:在A结点的右孩子的右子树中插入新结点导致A结点不平衡->需要左旋
以上述图片为例,
-
灰色方形的框表示结点的子树,该子树的高度用H表示,其中这些子树都是平衡的
-
BL表示B结点的左子树,BR表示B结点的右子树,AL表示A结点的左子树
(本例中假设在插入新结点之后以A结点为根结点的子树是"最小不平衡子树",且AL、BL、BR这三个部分的高度最开始必须都是相同的,原理同"LL里的思考")
上述图片中最左边的二叉树中A结点的右子树是以B结点为根结点的树,其中BL的高度为H,BR的高度也为H,算上B结点,根据二叉树的定义可知以B结点为根结点的树的高度为H+1(加的1是B结点的高度),A结点的左子树是AL,高度为H,
所以A结点的平衡因子=H-(H+1)=-1,所以A结点是平衡的;B结点的平衡因子=H-H=0,所以B结点是平衡的,
此时在A结点的右孩子即B结点的右子树BR中插入一个新结点(上述图片中间的二叉树),会导致A结点不平衡,这是因为B结点的右子树BR变高了,BR的高度变为了H+1,所以A结点的右子树整体来看高度变为了H+2,而A结点的左子树AL的高度仍保持为H,所以此时A结点的平衡因子=H-(H+2)=-2,导致A结点不平衡;B结点的平衡因子=H-(H+1)=-1,所以B结点是平衡的,
(注:假定以A结点为根结点的二叉树就是最小不平衡子树)
因此需要调整以A结点为根结点的二叉树(即上述图片中最中间的二叉树)使其保持平衡,
如下图:
如上图,
要调整以A结点为根结点的二叉树即"最小不平衡子树"(即上述图片中最中间的二叉树)使其平衡,
调整的结果需要满足:
-
恢复"最小不平衡子树"的平衡
-
使得"最小不平衡子树"在调整之后依然满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"
在本例中结点的大小关系为AL<A<BL<B<BR(注:以A结点为根结点的二叉树是二叉排序树,在A结点的右子树中的每一个结点的值都大于结点A的值,比如BL>A,B>A),
使得以A结点为根结点的二叉树即"最小不平衡子树"恢复平衡的处理方法如下:
步骤一:让B结点左旋->具体的做法就是让B结点向左上旋转代替A结点成为该二叉树的根结点,B结点的左子树就是以A结点为根结点的树;
步骤二:处理子树AL、BL、BR->
-
首先看子树BR,BR之前是B结点的右子树,BR的值大于B结点值(BR>B),所以只能把BR连接在B结点的右子树,因为要保证"左子树结点值<根结点值<右子树结点值"
-
再看子树AL,AL之前是A结点的左子树,有AL的值小于A结点的值(AL<A),所以AL必须作为A结点的左子树
-
最后看子树BL,BL之前是B结点的左子树,现在B结点的左子树已经挂了A结点,那么BL就不能作为B结点的左子树了,但是有BL的值大于A结点的值且小于B结点的值(B>BL>A),所以可以把子树BL当作A结点的右子树(BL在B结点的左子树中)
这样就可以既保证二叉排序树的特性,同时BR这个部分的高度是H+1即成功插入了新结点,
经过上述的调整之后(上述图片最右边的树)B结点的左子树的高度为H+1、右子树BR的高度为H+1,即B结点的平衡因子=(H+1)-(H+1)=0,
A结点的左子树AL的高度为H、右子树BL的高度为H,即A结点的平衡因子=H-H=0,
可知以A结点为根结点的子树和以B结点为根结点的子树都保持了平衡,
这样一来就可以让"最小不平衡子树"恢复平衡,同时又保持二叉排序树的特性。
4.RR的代码实现:
以上述图片中的二叉排序树为例,
-
f指针(father的缩写)指向A结点即f指针表示A结点;A结点的左子树为f->lchild;A结点的右子树为f->rchild(注:f指针始终指向A结点,不会改变)
-
p指针指向B结点即p指针表示B结点;B结点的左子树为p->lchild;B结点的右子树为p->rchild(注:p指针始终指向B结点,不会改变)
-
gf指针(grandfather的缩写)指向整个二叉树的根结点的父结点;由于整个二叉树的根结点可能是其父结点的左孩子,也可能是右孩子,所以整个二叉树的根结点既可能是gf->lchild,也可能是gf->rchild,因此gf->lchild或gf->rchild就表示整个二叉树的根结点(注:整个二叉树的根结点未必是A结点,也可能是B结点)
根据之前的分析,要让RR的情况恢复平衡,需要B结点左旋,旋转的结果就是新的二叉树,其中->
-
根结点从A结点变为B结点
-
A结点的左子树AL没变,A结点的右子树从以B结点为根结点的树变为BL
-
B结点的左子树从BL变为以A结点为根结点的树,B结点的右子树BR没变
各个结点的子树情况 | 二叉树未"右旋"时 | 二叉树"右旋"后 |
---|---|---|
整棵二叉树的根结点即gf->lchild或gf->rchild | gf->lchild或gf->rchild的值为A结点即f | gf->lchild或gf->rchild的值为B结点即p |
A结点的左子树即f->lchild | f->lchild是子树AL | f->lchild是子树AL |
A结点的右子树即f->rchild | f->rchild是以B结点为根结点的子树即p | f->rchild是子树BL |
B结点的左子树即p->lchild | p->lchild是子树BL | p->lchild是以A结点为根结点的子树即f |
B结点的右子树即p->rchild | p->rchild是子树BR | p->rchild是子树BR |
对于代码的书写只需要针对发生改变的子树和结点即可,还需要注意其中的逻辑(代码不唯一):
步骤一:对于A结点,由于A结点只有右子树f->rchild发生了改变即从以B结点为根结点的子树p变为子树BL,所以可以是f->rchild = p->lchild;
步骤二:对于B结点,由于B结点只有左子树p->lchild发生了改变即从子树BL变为以A结点为根结点的子树f,所以可以是p->lchild = f;
步骤三:对于整棵二叉树,只有根结点发生了改变即从A结点f变为B结点p,所以可以是gf->lchild = p或gf->rchild = p;,
这样就可以实现"左旋"操作,同时可以保证这棵二叉树依然保持二叉排序树的特性。
四.调整"最小不平衡子树"->LR和RL:(代码类似LL和RR,自行推理即可)
1.LR:在A结点的左孩子的右子树中插入新结点导致A结点不平衡
以上述图片为例,
-
灰色方形的框表示结点的子树,该子树的高度用H表示,其中这些子树都是平衡的
-
BL表示B结点的左子树,BR表示B结点的右子树,AR表示A结点的右子树
(本例中假设在插入新结点之后以A结点为根结点的子树是"最小不平衡子树",且AR、BL、BR这三个部分的高度最开始必须都是相同的,原理同"LL里的思考")
上述图片中最左边的二叉树中A结点的左子树是以B结点为根结点的树,其中BL的高度为H,BR的高度也为H,算上B结点,根据二叉树的定义可知以B结点为根结点的树的高度为H+1(加的1是B结点的高度),A结点的右子树是AR,高度为H,
所以A结点的平衡因子=(H+1)-H=1,所以A结点是平衡的;B结点的平衡因子=H-H=0,所以B结点是平衡的,
此时在A结点的左孩子即B结点的右子树BR(上述图片打错了)中插入一个新结点(上述图片中间的二叉树),会导致A结点不平衡,这是因为B结点的右子树BR变高了,BR的高度变为了H+1,所以A结点的左子树整体来看高度变为了H+2,而A结点的右子树AR的高度仍保持为H,所以此时A结点的平衡因子=(H+2)-H=2,导致A结点不平衡;B结点的平衡因子=H-(H+1)=-1,所以B结点是平衡的,
(注:假定以A结点为根结点的二叉树就是最小不平衡子树)
因此需要调整以A结点为根结点的二叉树使其保持平衡->为了方便探讨,需要把B结点的右子树BR展开,
如下图:
如上图,
现在把B结点的右子树BR展开,
假设子树BR是以C结点为根结点的二叉树,C结点的左子树为CL,C结点的右子树为CR,
最开始在上述图片最左边的二叉树中由于子树BR的高度为H,所以子树CL、CR的高度都为H-1(CL、CR的高度最开始必须是相同的,原理同"LL里的思考")->其中A结点的平衡因子=(H+1)-H=1即A结点是平衡的;B结点的平衡因子=H-H=0即B结点是平衡的;C结点的平衡因子=(H-1)-(H-1)=0即C结点是平衡的;
现在在子树BR中插入新结点之后(上述图片最右边的二叉树),子树BR的高度变为H+1,这里可以假设把新结点插在了C结点的右子树CR,导致子树CR的高度变为了H(新结点也可以插在了C结点的左子树CL,导致子树CL的高度变为了H,无论是哪种情况,最终的处理方法都是一样的,这里先假定插入的新结点在子树CR上)->其中A结点的平衡因子=(H+2)-H=2即A结点是不平衡的;B结点的平衡因子=H-(H+1)=-1即B结点是平衡的;C结点的平衡因子=(H-1)-H=-1即C结点是平衡的;
使得"最小不平衡子树"即以A结点为根结点的子树(上述图片最右边的二叉树)恢复平衡的处理方法如下图:
如上图:
要让"最小不平衡子树"即以A结点为根结点的子树(上述图片最右边的二叉树)恢复平衡,
核心就是让该"最小不平衡子树"中的C结点先"左旋",让C结点顶替B结点的位置,再让C结点"右旋",让C结点顶替A结点的位置,
其中结点的大小关系为BL<B<CL<C<CR<A<AR(注:以A结点为根结点的二叉树是二叉排序树,在A结点的左子树中的每一个结点的值都小于结点A的值,比如CL<A,CR<A;以B结点为根结点的二叉树也是二叉排序树,在B结点的右子树中的每一个结点的值都大于结点B的值,比如B<C,B<CL,B<CR),
具体操作如下图:
如上图,
要调整以A结点为根结点的二叉树即"最小不平衡子树"使其平衡,
调整的结果需要满足:
-
恢复"最小不平衡子树"的平衡
-
使得"最小不平衡子树"在调整之后依然满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"
在本例中结点的大小关系为BL<B<CL<C<CR<A<AR,
使得以A结点为根结点的二叉树即"最小不平衡子树"恢复平衡的处理方法如下:
步骤一:让C结点左旋->具体的做法就是让C结点向左上旋转代替B结点,C结点的左子树就是以B结点为根结点的树;
步骤二:处理子树BL、CL、CR、AR->
-
首先看子树BL,BL之前是B结点的左子树,BL的值小于B结点值(BL<B),所以只能把BL连接在B结点的左子树,因为要保证"左子树结点值<根结点值<右子树结点值"
-
再看子树CL,CL之前是C结点的左子树,现在C结点的左子树已经挂了B结点,那么CL就不能作为C结点的左子树了,但是有CL的值大于B结点的值(B<CL),所以可以把子树CL当作B结点的右子树
-
再看子树CR,CR之前是C结点的右子树,有CR的值大于C结点的值(C<CR),所以CR继续作为C结点的右子树
-
最后看子树AR,AR之前是A结点的右子树,有AR的值大于A结点的值(A<AR),所以AR继续作为A结点的右子树
此时得到了上述图片中最中间的二叉树->其中A结点的平衡因子=(H+2)-H=2即A结点是不平衡的;B结点的平衡因子=H-(H-1)=1即B结点是平衡的;C结点的平衡因子=(H+1)-H=1即C结点是平衡的,只有A结点不平衡,所以继续调整使得以A结点为根结点的二叉树保持平衡,
步骤三:(上述图片中最中间的二叉树)让C结点右旋->具体的做法就是让C结点向右上旋转代替A结点,C结点的右子树就是以A结点为根结点的树;
步骤四:处理子树BL、CL、CR、AR->
-
首先看子树BL,BL之前是B结点的左子树,BL的值小于B结点值(BL<B),所以只能把BL连接在B结点的左子树,因为要保证"左子树结点值<根结点值<右子树结点值"
-
再看子树CL,CL之前是B结点的右子树,有CL的值大于B结点的值(B<CL),所以CL继续作为B结点的右子树
-
再看子树CR,CR之前是C结点的右子树,现在C结点的右子树已经挂了A结点,那么CR就不能作为C结点的右子树了,但是有CR的值小于A结点的值(CR<A),所以可以把子树CR当作A结点的左子树
-
最后看子树AR,AR之前是A结点的右子树,有AR的值大于A结点的值(A<AR),所以AR继续作为A结点的右子树
这样就可以既保证二叉排序树的特性,同时BR这个部分的高度是H+1即成功插入了新结点,
经过上述的调整之后(上述图片最右边的树)B结点的左子树BL的高度为H、右子树CL的高度为H-1,即B结点的平衡因子=H-(H-1)=1,
A结点的左子树CR的高度为H、右子树AR的高度为H,即A结点的平衡因子=H-H=0,
C结点的左子树的高度为H+1、右子树的高度为H+1,即C结点的平衡因子=(H+1)-(H+1)=0,
可知以A结点为根结点的子树、以B结点为根结点的子树和以C结点为根结点的子树都保持了平衡,
这样一来就可以让"最小不平衡子树"恢复平衡,同时又保持二叉排序树的特性。
注意:
如上图,
本例假定插入的新结点在子树CR上,
实际上也有可能是插入在子树CL上,这种情况的处理方式同子树CL,
也可以保证使所有的结点都恢复平衡。
2.RL:在A结点的右孩子的左子树中插入新结点导致A结点不平衡
以上述图片为例,
-
灰色方形的框表示结点的子树,该子树的高度用H表示,其中这些子树都是平衡的
-
BL表示B结点的左子树,BR表示B结点的右子树,AL表示A结点的左子树
(本例中假设在插入新结点之后以A结点为根结点的子树是"最小不平衡子树",且AL、BL、BR这三个部分的高度最开始必须都是相同的,原理同"LL里的思考")
上述图片中最左边的二叉树中A结点的右子树是以B结点为根结点的树,其中BL的高度为H,BR的高度也为H,算上B结点,根据二叉树的定义可知以B结点为根结点的树的高度为H+1(加的1是B结点的高度),A结点的左子树是AL,高度为H,
所以A结点的平衡因子=H-(H+1)=-1,所以A结点是平衡的;B结点的平衡因子=H-H=0,所以B结点是平衡的,
此时在A结点的右孩子即B结点的左子树BL中插入一个新结点(上述图片中间的二叉树),会导致A结点不平衡,这是因为B结点的左子树BL变高了,BL的高度变为了H+1,所以A结点的右子树整体来看高度变为了H+2,而A结点的左子树AL的高度仍保持为H,所以此时A结点的平衡因子=H-(H+2)=-2,导致A结点不平衡;B结点的平衡因子=(H+1)-H=1,所以B结点是平衡的,
(注:假定以A结点为根结点的二叉树就是最小不平衡子树)
因此需要调整以A结点为根结点的二叉树使其保持平衡->为了方便探讨,需要把B结点的左子树BL展开,
如下图:
如上图,
现在把B结点的左子树BL展开,
假设子树BL是以C结点为根结点的二叉树,C结点的左子树为CL,C结点的右子树为CR,
最开始在上述图片最左边的二叉树中由于子树BL的高度为H,所以子树CL、CR的高度都为H-1(CL、CR的高度最开始必须是相同的,原理同"LL里的思考")->其中A结点的平衡因子=H-(H+1)=-1即A结点是平衡的;B结点的平衡因子=H-H=0即B结点是平衡的;C结点的平衡因子=(H-1)-(H-1)=0即C结点是平衡的;
现在在子树BL中插入新结点之后(上述图片最右边的二叉树),子树BL的高度变为H+1,这里可以假设把新结点插在了C结点的左子树CL,导致子树CL的高度变为了H(新结点也可以插在了C结点的右子树CR,导致子树CR的高度变为了H,无论是哪种情况,最终的处理方法都是一样的,这里先假定插入的新结点在子树CL上)->其中A结点的平衡因子=H-(H+2)=-2即A结点是不平衡的;B结点的平衡因子=(H+1)-H=1即B结点是平衡的;C结点的平衡因子=H-(H-1)=1即C结点是平衡的;
使得"最小不平衡子树"即以A结点为根结点的子树(上述图片最右边的二叉树)恢复平衡的处理方法如下图:
如上图:
要让"最小不平衡子树"即以A结点为根结点的子树(上述图片最右边的二叉树)恢复平衡,
核心就是让该"最小不平衡子树"中的C结点先"右旋",让C结点顶替B结点的位置,再让C结点"左旋",让C结点顶替A结点的位置,
其中结点的大小关系为AL<A<CL<C<CR<B<BR(注:以A结点为根结点的二叉树是二叉排序树,在A结点的右子树中的每一个结点的值都大于结点A的值,比如A<CL,A<C,A<B;以B结点为根结点的二叉树也是二叉排序树,在B结点的左子树中的每一个结点的值都小于结点B的值,比如C<B,CL<B,CR<B),
具体操作如下图:
如上图,
要调整以A结点为根结点的二叉树即"最小不平衡子树"使其平衡,
调整的结果需要满足:
-
恢复"最小不平衡子树"的平衡
-
使得"最小不平衡子树"在调整之后依然满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"
在本例中结点的大小关系为AL<A<CL<C<CR<B<BR,
使得以A结点为根结点的二叉树即"最小不平衡子树"恢复平衡的处理方法如下:
步骤一:让C结点右旋->具体的做法就是让C结点向右上旋转代替B结点,C结点的右子树就是以B结点为根结点的树;
步骤二:处理子树AL、CL、CR、BR->
-
首先看子树AL,AL之前是A结点的左子树,AL的值小于A结点值(AL<A),所以只能把AL连接在A结点的左子树,因为要保证"左子树结点值<根结点值<右子树结点值"
-
再看子树CL,CL之前是C结点的左子树,有CL的值小于C结点的值(CL<C),所以CL继续作为C结点的左子树
-
再看子树CR,CR之前是C结点的右子树,现在C结点的右子树已经挂了B结点,那么CR就不能作为C结点的右子树了,但是有CR的值小于B结点的值(CR<B),所以可以把子树CR当作B结点的左子树
-
最后看子树BR,BR之前是B结点的右子树,有BR的值大于B结点的值(B<BR),所以BR继续作为B结点的右子树
此时得到了上述图片中最中间的二叉树->其中A结点的平衡因子=H-(H+2)=-2即A结点是不平衡的;B结点的平衡因子=(H-1)-H=-1即B结点是平衡的;C结点的平衡因子=H-(H+1)=-1即C结点是平衡的,只有A结点不平衡,所以继续调整使得以A结点为根结点的二叉树保持平衡,
步骤三:(上述图片中最中间的二叉树)让C结点左旋->具体的做法就是让C结点向左上旋转代替A结点,C结点的左子树就是以A结点为根结点的树;
步骤四:处理子树AL、CL、CR、BR->
-
首先看子树AL,AL之前是A结点的左子树,AL的值小于A结点值(BL<B),所以只能把AL连接在A结点的左子树,因为要保证"左子树结点值<根结点值<右子树结点值"
-
再看子树CL,CL之前是C结点的左子树,现在C结点的左子树已经挂了A结点,那么CL就不能作为C结点的左子树了,但是有CL的值大于A结点的值(CL>A),所以可以把子树CL当作A结点的右子树
-
再看子树CR,CR之前是B结点的左子树,有CR的值小于B结点的值(CR<B),所以CR继续作为B结点的左子树
-
最后看子树BR,BR之前是B结点的右子树,有BR的值大于B结点的值(B<BR),所以BR继续作为B结点的右子树
这样就可以既保证二叉排序树的特性,同时BL这个部分的高度是H+1即成功插入了新结点,
经过上述的调整之后(上述图片最右边的树)B结点的左子树CR的高度为H-1、右子树BR的高度为H,即B结点的平衡因子=(H-1)-H=-1,
A结点的左子树AL的高度为H、右子树CL的高度为H,即A结点的平衡因子=H-H=0,
C结点的左子树的高度为H+1、右子树的高度为H+1,即C结点的平衡因子=(H+1)-(H+1)=0,
可知以A结点为根结点的子树、以B结点为根结点的子树和以C结点为根结点的子树都保持了平衡,
这样一来就可以让"最小不平衡子树"恢复平衡,同时又保持二叉排序树的特性。
注意:
如上图,
本例假定插入的新结点在子树CL上,
实际上也有可能是插入在子树CR上,这种情况的处理方式同子树CL,
也可以保证使所有的结点都恢复平衡。
五.LL、RR、LR和RL的总结:
-
只有左孩子才能"右旋";只有右孩子才能"左旋"->每一次旋转都会导致这个孩子变成爹,爹变成孩子
六.填坑:
以上述图片中最左边的二叉排序树为例,
此时已经插入了关键字为67的结点,导致关键字为70的结点不平衡(平衡因子为2),导致该结点不平衡的原因是在其左孩子的左子树上插入了一个新结点67,也就是"LL"型,
这种情况应该让不平衡结点即关键字为70的结点的左孩子进行"右旋",也就是关键字为68的结点替代关键字为70的结点的位置,然后关键字为70的结点成为关键字为68的结点的右孩子,67结点连在68结点的左边,
之前说过68的右子树要成为70的左子树,但原本68的右子树是一个空树NULL,把68的右子树连接在70结点的左子树后相当于结点70的左子树为NULL,因此经过"右旋"操作之后得到的结果如上述图片最右边的二叉树,
现在要填的坑是,刚开始说过只要把"最小不平衡子树"恢复平衡之后其他所有的结点都会恢复平衡,现在来解释一下为什么会这样,
如下图:
如上图,
之前介绍了"LL"、"RR"、"LR"、"RL"四种情况,
对于这四类情况抽象的假设,
刚开始整棵二叉树的高度为H+2,
插入新结点之后会导致不平衡,这棵"最小不平衡子树"的高度为H+3,
但是经过旋转一系列的处理之后二叉树的整体高度又恢复了H+2,
如下图:
如上图:
所以这就能解释为什么只需要调整"最小不平衡子树"就能恢复整个二叉树的平衡,
"最小不平衡子树"变的不平衡是因为插入新结点后它的高度加1,但是经过调整之后"最小不平衡子树"的高度又会恢复成原来的高度,
所以对于"最小不平衡子树"的根结点的父结点来说,只要把该"最小不平衡子树"的高度恢复原状,
"最小不平衡子树"的根结点的父结点的平衡因子也就会恢复原状,同样的再往上的祖先结点也都会跟着恢复原状,
因此只要把"最小不平衡子树"的高度恢复原状,那么其他结点的平衡因子也都会依次恢复,
所以在这种插入操作中只需要调整"最小不平衡子树","最小不平衡子树"恢复平衡后,其他结点就都可以恢复平衡。
七.练习:
1.例一:
以上述图片最左边的二叉排序树为例,
现在要插入关键字为90的结点,根据二叉排序树的特性可知应该连接到关键字为70的结点的右子树上,最终得到上述图片中最右边的二叉树,
插入关键字为90的结点后,它的父结点即关键字为70的结点以及其祖先结点的平衡因子都可能受到影响,
所以可以从下到上依次检查关键字为90的结点的父结点和祖先节点的平衡因子,
当检查到关键字为66的结点后,发现是第一个不平衡的结点,平衡因子为-2,
因此以66结点为根结点的子树就是"最小不平衡子树",接下来只需要调整该"最小不平衡子树"使其平衡即可,
66结点不平衡的原因是在它的右孩子68结点的右子树中插入了一个新结点90,也就是"RR"型,
如下图:
如上图:
处理方式应该让上述图片中最右边的二叉树的不平衡结点66的右孩子68结点"左旋",替代66结点的位置,
如下图:
如上图,
上述图片中最左边的二叉树的不平衡结点66的右孩子68结点"左旋","左旋"操作会导致68结点的左孩子67结点变成66结点的右孩子(详情见本篇"RR"),最终得到上述图片中最右边的二叉树,
最后一定要验证得出的二叉树是否满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值",如果不满足说明处理有问题,
经过调整之后,这棵"最小不平衡子树"就恢复了平衡,同时其他的结点的平衡因子也恢复了平衡。
2.例二:
以上述图片最左边的二叉排序树为例,
现在要插入关键字为63的结点,根据二叉排序树的特性可知应该连接到关键字为60的结点的右子树上,最终得到上述图片中最右边的二叉树,
插入关键字为63的结点后,它的父结点即关键字为60的结点以及其祖先结点的平衡因子都可能受到影响,
所以可以从下到上依次检查关键字为63的结点的父结点和祖先节点的平衡因子,
当检查到关键字为50的结点后,发现是第一个不平衡的结点,平衡因子为-2,
因此以50结点为根结点的子树就是"最小不平衡子树",接下来只需要调整该"最小不平衡子树"使其平衡即可,
50结点不平衡的原因是在它的右孩子68结点的左子树中插入了一个新结点63,也就是"RL"型,
这种类型要做的处理就是把不平衡结点即50结点的右孩子68结点的左孩子66结点先"右旋",再"左旋",
如下图:
如上图:
处理方式是首先让上述图片中最左边的二叉树的结点66右旋替代结点68的位置,结点68成为结点66的右孩子,
结点66的右孩子67结点变为结点68的左子树(详情见本篇"RL"),
最终得到上述图片中最右边的二叉树,
如下图:
如上图,
需要继续"左旋"操作,让上述图片中最左边的二叉树的结点66进行"左旋"替代结点50的位置,左旋之后结点66的左子树(以结点60为根结点的子树)应该成为结点50的右子树,结点50成为结点66的左孩子,最终得到上述图片中最右边的二叉树,
这样的话整棵树恢复了平衡,同时依然保持了二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"。
最后一定要验证得出的二叉树是否满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值",如果不满足说明处理有问题,
经过调整之后,这棵"最小不平衡子树"就恢复了平衡,同时其他的结点的平衡因子也恢复了平衡。
3.例三:
以上述图片最左边的二叉排序树为例,
现在要插入关键字为57的结点,根据二叉排序树的特性可知应该连接到关键字为55的结点的右子树上,最终得到上述图片中最右边的二叉树,
插入关键字为57的结点后,它的父结点即关键字为55的结点以及其祖先结点的平衡因子都可能受到影响,
所以可以从下到上依次检查关键字为57的结点的父结点和祖先节点的平衡因子,
当检查到关键字为66的结点后,发现是第一个不平衡的结点,平衡因子为2,
因此以66结点为根结点的子树就是"最小不平衡子树",接下来只需要调整该"最小不平衡子树"使其平衡即可,
66结点不平衡的原因是在它的左孩子50结点的右子树中插入了一个新结点57,也就是"LR"型,
这种类型要做的处理就是把不平衡结点即66结点的左孩子50结点的右孩子60结点先"左旋",再"右旋",
如下图:
如上图:
将上述图片中最左边的二叉树中的60结点先"左旋",再"右旋",最终得到上述图片中最右边的二叉树(详情见本篇"LR"),
这样的话整棵树恢复了平衡,同时依然保持了二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"。
最后一定要验证得出的二叉树是否满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值",如果不满足说明处理有问题,
经过调整之后,这棵"最小不平衡子树"就恢复了平衡,同时其他的结点的平衡因子也恢复了平衡。
八.查找效率分析:
如上图,
在一棵二叉排序树中进行查找操作,查找效率主要来自于该二叉排序树的高度(详情见"7.5.二叉排序树(英文缩写为BST,又名二叉查找树)"),
假设当前二叉排序树的高度为h,那么查找任何一个关键字最多需要对比h次(详情见"7.5.二叉排序树(英文缩写为BST,又名二叉查找树)"),
所以在二叉排序树中进行查找操作的时间复杂度不会超过O(h),也就意味着平均查找长度即ASL不会超过O(h),
由此可知分析平衡二叉树的查找效率,其实就是分析该平衡二叉树的高度到底有多高,
基于平衡二叉树的特性,
用N(h)表示深度为h的平衡二叉树含有的最少的结点个数,
当h为0时该平衡二叉树为空树,那么该平衡二叉树的结点个数N0为0,
当h为1时该平衡二叉树只有1个根结点,那么该平衡二叉树的结点个数N1为1,
当h为2时该平衡二叉树中,它的结点最少有2个即根结点和其左孩子或者右孩子,那么该平衡二叉树的结点个数N2为2,
再基于平衡二叉树"树上任一结点的左子树和右子树的高度只差不超过1"的递归特性可以得出N(h)=N(h-1)+N(h-2)+1,
即高度为h的平衡二叉树的结点个数最少的情况应该是1个根结点,加其左子树,该左子树的高度为h-1,同时要保证左子树的结点数最小,再加其右子树,该右子树的高度为h-2,并且也要保证右子树的结点数最小,
所以可知N3为4,N4为7,N5为12,以此类推,
因此如果一棵平衡二叉树的结点总数N为9,那么它的高度h最大为4->它的高度不可能为3,因为现在有9个结点,已经超出了N4;它的高度要到5的话,最少需要12个结点,
所以对于总共有9个结点的平衡二叉树来说,在这棵二叉树上查找一个关键字,最坏的情况也就需要对比4次关键字。
如果平衡二叉树的结点总数为N,高度为h,基于N(h)=N(h-1)+N(h-2)+1这个公式可以证明平衡二叉树的最大高度是O(log₂N)的数量级(推导过程可以参考"5.4.二叉树的性质"),
而平衡二叉树的最大高度又反应了在最坏情况下查找操作的时间复杂度,因此最坏情况下平衡二叉树的ASL或者查找操作的时间复杂度为O(log₂N)->推导过程很复杂,如下图: