一.平衡二叉树的定义:

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结点为根结点的二叉树即"最小不平衡子树"(即上述图片中最中间的二叉树)使其平衡,

调整的结果需要满足:

  1. 恢复"最小不平衡子树"的平衡

  2. 使得"最小不平衡子树"在调整之后依然满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"

在本例中结点的大小关系为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->rchildgf->lchild或gf->rchild的值为A结点即fgf->lchild或gf->rchild的值为B结点即p
A结点的左子树即f->lchildf->lchild是以B结点为根结点的子树即pf->lchild是子树BR
A结点的右子树即f->rchildf->rchild是子树ARf->rchild是子树AR
B结点的左子树即p->lchildp->lchild是子树BLp->lchild是子树BL
B结点的右子树即p->rchildp->rchild是子树BRp->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结点为根结点的二叉树即"最小不平衡子树"(即上述图片中最中间的二叉树)使其平衡,

调整的结果需要满足:

  1. 恢复"最小不平衡子树"的平衡

  2. 使得"最小不平衡子树"在调整之后依然满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"

在本例中结点的大小关系为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->rchildgf->lchild或gf->rchild的值为A结点即fgf->lchild或gf->rchild的值为B结点即p
A结点的左子树即f->lchildf->lchild是子树ALf->lchild是子树AL
A结点的右子树即f->rchildf->rchild是以B结点为根结点的子树即pf->rchild是子树BL
B结点的左子树即p->lchildp->lchild是子树BLp->lchild是以A结点为根结点的子树即f
B结点的右子树即p->rchildp->rchild是子树BRp->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结点为根结点的二叉树即"最小不平衡子树"使其平衡,

调整的结果需要满足:

  1. 恢复"最小不平衡子树"的平衡

  2. 使得"最小不平衡子树"在调整之后依然满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"

在本例中结点的大小关系为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结点为根结点的二叉树即"最小不平衡子树"使其平衡,

调整的结果需要满足:

  1. 恢复"最小不平衡子树"的平衡

  2. 使得"最小不平衡子树"在调整之后依然满足二叉排序树的特性即"左子树结点值<根结点值<右子树结点值"

在本例中结点的大小关系为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)->推导过程很复杂,如下图:


九.总结:


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/88745.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/88745.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/88745.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV CUDA模块设备层-----将指向共享内存(shared memory)的指针封装成一个 tuple函数smem_tuple()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 OpenCV的cv::cudev模块中的一个用于 CUDA 编程的辅助函数&#xff0c;用于将指向共享内存&#xff08;shared memory&#xff09;的指针封装成一…

paddlepaddle在RTX40系安装注意事项

1 安装简介 1.1 安装注意事项 显卡型号&#xff1a;RTX4090 驱动版本&#xff1a;550.54.14 宿主机cuda版本&#xff1a;12.4 安装方式&#xff1a;conda 注意cuda和cudnn的搭配 最初安装是为了使用PaddleOCR&#xff0c;根据官网提示需要安装cuda和cudnn。这里最关键的就是针…

车载以太网-组播

目录 车载以太网中的组播:从原理到车载应用**一、组播的核心概念与车载网络价值****二、车载以太网组播的关键协议与机制**1. **组播IP地址管理(IGMP协议)**2. **组播数据链路层实现(MAC地址映射)****三、车载以太网组播的典型应用场景**1. **自动驾驶与传感器数据分发**2…

【雅思播客013】what do you do

【dialog】 A: Oh, look, there’s Veronica and her boyfriend.She’s always going on about him at the office. Oh, great, they saw us. They’re coming this way. B: Oh, man... C: Jessica! Arthur! Hi! I’d like you to meet my boyfriend Greg, he’s the VP. of q…

Freebsd 14.2系统下 wifi网卡硬件驱动软件配置调试大全

Freebsd 14.2系统下&#xff0c;网卡是AX200 先检查网卡sysctl net.wlan.devices sysctl net.wlan.devices 能识别出已经安装的 sysctl net.wlan.devices net.wlan.devices: iwlwifi0配置wlan0 # ifconfig wlan0 create wlandev iwlwifi0 # ifconfig wlan0 up # ifconfig …

Python打卡:Day39

知识点回顾 图像数据的格式&#xff1a;灰度和彩色数据模型的定义显存占用的4种地方 模型参数梯度参数优化器参数数据批量所占显存神经元输出中间状态 batchisize和训练的关系 浙大疏锦行

使用 GcExcel .NET 将 Excel 导出为 PDF

引言 在企业级应用开发中&#xff0c;经常需要将Excel数据导出为PDF格式以便于共享和打印。GrapeCity Documents for Excel&#xff08;简称GcExcel&#xff09;作为一款高性能的.NET Excel组件&#xff0c;提供了强大的PDF导出功能。本文将详细介绍如何使用GcExcel .NET实现E…

每日算法刷题Day39 6.26:leetcode前缀和2道题,用时1h20min

8. 2055.蜡烛之间的盘子(中等,学习替换查询区间) 2055. 蜡烛之间的盘子 - 力扣&#xff08;LeetCode&#xff09; 思想 1.给你一个长桌子&#xff0c;桌子上盘子和蜡烛排成一列。给你一个下标从 0 开始的字符串 s &#xff0c;它只包含字符 * 和 | &#xff0c;其中 * 表示一…

jrebel 下载,安装,激活步骤

参考地址&#xff1a; [笔记] 最新版 - JRebel 插件激活与配置教程 : 高效开发的必备指南_jrebel激活-CSDN博客https://blog.csdn.net/LuChangQiu/article/details/145547828 1、下载 2、激活地址&#xff1a; http://42.193.18.168:8088 ### 捡个便宜 - 交朋友吧 ###https://…

uniapp使用plus调取蓝牙/usb打印

安卓使用usb调取打印机 /*** 安卓usb调取打印机*param { string | bytes[] } html 传入的打印内容*传入一段文本或一个bytes数组* returns*/ export const printUsb (html) > {return new Promise((resolve, reject) > {if (!window.plus) return reject(new Error(&qu…

区块链数据结构:区块与链式结构编码

目录 区块链数据结构:区块与链式结构编码引言:区块链的骨架1. 区块链数据结构解析1.1 区块结构组成1.2 区块链可视化结构2. 区块核心组件详解2.1 区块头字段说明2.2 Merkle树结构2.3 工作量证明机制3. Python实现区块链数据结构3.1 区块类实现3.2 区块链类实现3.3 区块链演示…

阿里推出 R1-Omni:将强化学习与可验证奖励(RLVR)应用于全模态大语言模型

从视频中识别情感涉及许多细微的挑战。仅依赖视觉或音频信号的模型&#xff0c;往往无法准确捕捉这两种模态之间的复杂相互作用&#xff0c;从而导致对情感内容的误解。一个关键难题在于可靠地结合视觉线索&#xff08;如面部表情或肢体语言&#xff09;与听觉信号&#xff08;…

【江科大】STM32F103C8T6 + TB6612 + N20编码器减速电机《03-增量式PID定速控制》(增量式PID,定时器输入捕获,定时器编码器)

STM32F103C8T6单片机+N20减速电机带霍尔编码器版PID闭环控制实验演示 STM32F103C8T6 实现的电机转速控制系统,基于 PWM 输出驱动、编码器采样反馈、以及增量式 PID 算法进行控制。 /*** @file Encoder.c* @brief 增量式编码器驱动程序* @details 使用TIM3定时器的编码器…

【论文阅读35】-PINN review(2021)

这篇综述全面回顾了物理信息机器学习 的原理、应用、软件实现、理论进展与未来发展趋势&#xff0c;这样即使数据稀疏、带噪&#xff0c;也能保证预测结果符合物理规律&#xff0c;适合解决偏微分方程正问题、反问题、非线性动力学和多物理耦合系统等科学计算场景。 作者信息&…

深度学习初探:聚焦 Transformer 与 LLM 的核心世界

文章目录 前言一、神经网络基础&#xff1a;智能的基石二、Transformer 架构&#xff1a;AI 新纪元的基石Transformer 的核心特性Transformer 的关键组件 三、 大语言模型概览总结 前言 人工智能的浪潮正以前所未有的力量重塑世界&#xff0c;而这场变革的核心引擎之一&#x…

【开发杂谈】Auto Caption:使用 Electron 和 Python 开发实时字幕显示软件

项目已开源到 GitHub&#xff0c;项目地址&#xff1a;HiMeditator/auto-captionhttps://github.com/HiMeditator/auto-caption 软件下载(Windows平台)&#xff1a;Releases HiMeditator/auto-captionhttps://github.com/HiMeditator/auto-caption/releases 你是否遇到过看外…

临床项目范围管理:确保项目聚焦与成功交付

一、核心目标 1.1 清晰定义项目边界 1.1.1 明确项目目标 明确项目具体目标、可交付成果、研究活动、纳入/排除标准、数据收集范围等,为项目规划、执行、监控和控制奠定基础。 1.1.2 防止范围蔓延 严格控制未经批准的变更,避免项目目标、活动或可交付成果超出最初约定,导致…

opi是什么

是的&#xff0c;当然可以&#xff01;您提出了一个非常好的问题。 opi 远不止是一个 NVIDIA 驱动安装器&#xff0c;它是一个非常强大的、专为 openSUSE 设计的**“超级安装助手”**或“智能搜索工具”。 它的主要目的就是为了解决一个常见问题&#xff1a;“我想安装一个软…

【Go语言-Day 9】指针基础:深入理解内存地址与值传递

Langchain系列文章目录 01-玩转LangChain&#xff1a;从模型调用到Prompt模板与输出解析的完整指南 02-玩转 LangChain Memory 模块&#xff1a;四种记忆类型详解及应用场景全覆盖 03-全面掌握 LangChain&#xff1a;从核心链条构建到动态任务分配的实战指南 04-玩转 LangChai…

如何使用 vue vxe-table 来实现一个产品对比表表格

如何使用 vue vxe-table 来实现一个产品对比表表格 查看官网&#xff1a;https://vxetable.cn 效果 代码 <template><div class"demo-page-wrapper"><vxe-grid v-bind"gridOptions"><template #img11><vxe-image src"h…