数据结构之AVL树(平衡二叉树)的理解

一.右旋和左旋

1.右旋(有右孩子则抛弃右孩子)

<1>平平无奇的右旋

 

上图就是一个平平无奇的右旋,简单来说就是:

  • 断开D和B的联系
  • 将B的右子树指向D,即D作为B的右子树(因为D比B大

  • 用B代替原先D的位置

<2>鸠占鹊巢的右旋(B本身有右子树)

若B本身有右子树,如:

此时B本身是有右子树的,对以D为结点的此最小平衡树进行右旋:

  • 将D的左子树指向B的右子树(B和D断开,把B的右子树作为D的左子树
  • 将B的右子树指向D(B和C断开,把D作为B的右子树
  • 用B代替原先D的位置

<3>通用右旋操作

<1>和<2>右旋合并到一种情况:

  • Step1:将D的左子树指向B的右子树(B的右子树成为D的左子树

      ①对于平平无奇的右旋,B的右子树为NULL,则D的左子树指向NULL

      ②对于鸠占鹊巢的右旋,B的右子树为C,则D的左子树指向C

  • Step2:将B的右子树指向D(D成为B的右子树

      ①对于平平无奇的右旋,B的右子树指向D

      ②对于鸠占鹊巢的右旋,B的右子树指向D

  • Step3:将B顶替原先D的位置(B成为新的中心

两者完全可以归并到一种情况,因此代码为:

//AVL树通用右旋操作
void R_Rotate(BiTree *P)
{
	BiTree L;
	L = (*P)->lchild;			    //定义L为P的左结点
	(*P)->lchild = L->rchild;		    //Step1:*P的左子树指向L的右子树(将L的右子树挂为*P的左子树)
	L->rchild = (*P);		            //Step2:L的右子树指向*P(将P挂为L的右子树)
	*P = L;					    //Step3:代替原有*P的位置
}

2.左旋(有左孩子则抛弃左孩子)

<1>平平无奇的左旋

 

上图就是一个平平无奇的左旋,简单来说就是:

  • 断开B和D的联系
  • 将D的左子树指向B,即B作为D的左子树(因为B比D小

  • 用D代替原先B的位置

<2>鸠占鹊巢的左旋(D本身有左子树)

若D本身有左子树,如:

此时D本身是有左子树的,对以B为结点的此最小平衡树进行右旋:

  • 将B的右子树指向D的左子树(B和D断开,把D的左子树作为B的右子树
  • 将D的左子树指向B(D和C断开,把B作为D的左子树
  • 用D代替原先B的位置

<3>通用左旋操作

<1>和<2>左旋合并到一种情况:

  • Step1:将B的右子树指向D的左子树(D的左子树成为B的右子树

      ①对于平平无奇的左旋,D的左子树为NULL,则B的右子树指向NULL

      ②对于鸠占鹊巢的左旋,D的左子树为C,则B的右子树指向C

  • Step2:将D的左子树指向B(B成为D的左子树

      ①对于平平无奇的左旋,D的左子树指向B

      ②对于鸠占鹊巢的左旋,D的左子树指向B

  • Step3:将D顶替原先B的位置(D成为新的中心

两者完全可以归并到一种情况,因此代码为:

//AVL树通用左旋操作
void L_Rotate(BiTree *P)
{
	BiTree R;
	R = (*P)->rchild;				//定义R为P的右结点
	(*P)->rchild = R->lchild;		        //Step1:*P的右子树指向R的左子树(将R的左子树挂为P的右子树)
	R->lchild = (*P);				//Step2:R的左子树指向*P(将P挂为R的左子树)
	(*P) = L;					//Step3:代替原有*P的位置
}

二.AVL有且只有的四种情况(LL,LR,RL,RR)

1.LL(Left-Left):单右旋

LL即Left-Left,这时候要进行:单右旋

LL型顾名思义,即插入A结点的L结点的L结点下(可以是LL的左或右都行,下图New为LL下的左子树)引起的不平衡,这种情况,要进行单右旋操作

此时的单右旋操作因为B本身有右结点,因此属于在一中介绍的右旋之鸠占鹊巢右旋

 

 拓展:

问题Q

LL类型本质上是在中心结点(A)Left--Left后的结点(上图结点7)后插入一个New结点,导致二叉树失衡

解决A

只需要对以结点(A)为中心的二叉树进行一次(单次)右旋操作,即可恢复平衡性

FAQ:

那么,上图中是在结点7之后的左结点插入了New,构成了LL-L,导致了二叉树的失衡,

实际上,在结点7之后的右结点插入New,构成LL-R,也会也会导致二叉树的失衡。

但是,我们都将其归结到LL型,因为这两种类型都是其子类型,且这两种类型都可以通过对:以结点(A)为中心的二叉树进行一次(单次)右旋操作恢复平衡性。

Summary总结:只要是LL型,那么对中心结点二叉树进行一次单右旋操作就可恢复平衡性


2.RR(Right-Right):单左旋

RR即Right-Right,这时候要进行:单左旋

RR型顾名思义,即插入A结点的R结点的R结点下(可以是RR的左或右都行,下图New为RR下的右子树)引起的不平衡,这种情况,要进行单左旋操作

此时的单左旋操作因为B本身有左结点,因此属于在一中介绍的左旋之鸠占鹊巢左旋

 

 拓展:

问题Q

RR类型本质上是在中心结点(A)Right-Right后的结点(上图结点7)后插入一个New结点,导致二叉树失衡

解决A

只需要对以结点(A)为中心的二叉树进行一次(单次)左旋操作,即可恢复平衡性

FAQ:

那么,上图中是在结点7之后的右结点插入了New,构成了RR-R下图),导致了二叉树的失衡

实际上,在结点7之后的左结点插入New,构成RR-L(下图),也会也会导致二叉树的失衡。

但是,我们都将其归结到RR型,因为这两种类型都是其子类型,且这两种类型都可以通过对:以结点(A)为中心的二叉树进行一次(单次)左旋操作恢复平衡性。

Summary总结:只要是RR型,那么对中心结点二叉树进行一次单左旋操作就可恢复平衡性


3.LR(Left-Right):双旋(先L后R)

LR即Left-Right型,这时候要进行:双旋(先对A的左子树进行L旋转,后对A为中心的全部R旋转)

LR型顾名思义,即插入A结点的L结点的R结点下(可以是LR下的L结点或R结点,也可以在LR下接着拓展L/R等,上图New为LR下的左子树)引起的不平衡,这种情况,要进行双旋操作

第一旋:对以A的左子树为根节点的树(即L后面结点,B-9-New)进行左旋

第二旋:再对以A为根节点的树(即全部结点)进行右旋

以上图为例,详细展开来说即为:

 

 

第一旋:对A的左子树(L结点后面的树B)进行左旋,因为结点9本身有左结点,因此这属于鸠占鹊巢的左旋;

旋转后就从L-R型变为了L-L型

第二旋:第一旋后的结果为L-L型,因此以A为根节点,进行单右旋,旋转后二叉树平衡

拓展:

问题Q

LR类型本质上是在中心结点(A)Left-Right后的结点后插入一个New结点,导致二叉树失衡

解决A

只需要对以结点A的左子树为根节点的二叉树进行一次左旋操作,再对A为根节点的二叉树进行一次右旋操作,即可恢复平衡性

FAQ:

那么,上图中是在结点9之后的左结点插入了New,构成了LR-L下图),导致了二叉树的失衡

实际上,在结点9之后的右结点插入New,构成LR-R(下图),也会也会导致二叉树的失衡。

其实可以更复杂一点,甚至可以衍生出:

LR-LL类(LR-LL,LR-LLL,LR-LLLL.....)

LR-LR类(LR-LR,LR-LRR,LR-LRRR...)

LR-RR类(LR-RR.LR-RRR,LR-RRRR...)

LR-RL类(LR-RL.LR-RLL,LR-RLLL...)

等等等等各种类型

但是我们都将其归结到LR型,因为都是其子类型,要明白:

  1. 只要是LR型,不论怎么衍生,其本质是不变的;

  2. 只要是LR型,就一定可以通过:Step1对中心节点左子树进行左旋,Step2对中心结点整体进行右旋恢复平衡;

Summary总结:只要是LR型,那么对中心结点左子树的二叉树进行一次左旋,再对中心结点二叉树进行一次右旋操作就可恢复平衡性


4.RL(Right-Left):双旋(先R后L)

RL即Right-Left型,这时候要进行:双旋(先对A的右子树进行R旋转,后对A为中心的二叉树进行L旋转)

RL型顾名思义,即插入A结点的R结点的L结点下(可以是RL下的L结点或R结点,也可以在RL下接着拓展L/R等,上图New为RL下的左子树)引起的不平衡,这种情况,要进行双旋操作

第一旋:对以A的右子树为根节点的树(即R后面结点,B-13-10-New)进行右旋

第二旋:再对以A为根节点的树(即全部结点)进行左旋

以上图为例,详细展开来说即为:

 

 

第一旋:对A的右子树(R结点后面的树B)进行单右旋,因为结点10本身无右结点,因此这属于平平无奇的右旋;

旋转后就从R-L型变为了R-R型

第二旋:第一旋后的结果为R-R型,因此以A为根节点,进行单左旋,旋转后二叉树平衡

拓展:

问题Q

RL类型本质上是在中心结点(A)Right-Left后的结点后插入一个New结点,导致二叉树失衡

解决A

只需要对以结点A的右子树为根节点的二叉树进行一次右旋操作,再对A为根节点的二叉树进行一次左旋操作,即可恢复平衡性

FAQ:

其实可以更复杂一点,甚至可以衍生出:

RL-LL类

RL-LR类

RL-RR类

RL-RL类

等等等等各种类型

但是我们都将其归结到RL型,因为都是其子类型,要明白:

  1. 只要是RL型,不论怎么衍生,其本质是不变的;

  2. 只要是RL型,就一定可以通过:Step1对中心节点右子树进行右旋,Step2对中心结点整体进行左旋恢复平衡;

Summary总结:只要是RL型,那么对中心结点右子树的二叉树进行一次右旋,再对中心结点二叉树进行一次左旋操作就可恢复平衡性


三.代码

#define LH +1 /*  左高 */ 
#define EH 0  /*  等高 */ 
#define RH -1 /*  右高 */ 

/*  对以指针T所指结点为根的二叉树作左平衡旋转处理 */
/*  本算法结束时,指针T指向新的根结点 */
void LeftBalance(BiTree *T)
{
	BiTree L, Lr;
	L = (*T)->lchild; /*  L指向T的左子树根结点 */
	switch (L->bf)
	{ /*  检查T的左子树的平衡度,并作对应平衡处理 */
	case LH: /*  新结点插入在T的左孩子的左子树上,要作单右旋处理 */
		(*T)->bf = L->bf = EH;
		R_Rotate(T);
		break;
	case RH: /*  新结点插入在T的左孩子的右子树上。要作双旋处理 */
		Lr = L->rchild; /*  Lr指向T的左孩子的右子树根 */
		switch (Lr->bf)
		{ /*  改动T及其左孩子的平衡因子 */
		case LH: (*T)->bf = RH;
			L->bf = EH;
			break;
		case EH: (*T)->bf = L->bf = EH;
			break;
		case RH: (*T)->bf = EH;
			L->bf = LH;
			break;
		}
		Lr->bf = EH;
		L_Rotate(&(*T)->lchild); /*  对T的左子树作左旋平衡处理 */
		R_Rotate(T); /*  对T作右旋平衡处理 */
	}
}

/*  对以指针T所指结点为根的二叉树作右平衡旋转处理, */
/*  本算法结束时,指针T指向新的根结点 */
void RightBalance(BiTree *T)
{
	BiTree R, Rl;
	R = (*T)->rchild; /*  R指向T的右子树根结点 */
	switch (R->bf)
	{ /*  检查T的右子树的平衡度。并作对应平衡处理 */
	case RH: /*  新结点插入在T的右孩子的右子树上。要作单左旋处理 */
		(*T)->bf = R->bf = EH;
		L_Rotate(T);
		break;
	case LH: /*  新结点插入在T的右孩子的左子树上,要作双旋处理 */
		Rl = R->lchild; /*  Rl指向T的右孩子的左子树根 */
		switch (Rl->bf)
		{ /*  改动T及其右孩子的平衡因子 */
		case RH: (*T)->bf = LH;
			R->bf = EH;
			break;
		case EH: (*T)->bf = R->bf = EH;
			break;
		case LH: (*T)->bf = EH;
			R->bf = RH;
			break;
		}
		Rl->bf = EH;
		R_Rotate(&(*T)->rchild); /*  对T的右子树作右旋平衡处理 */
		L_Rotate(T); /*  对T作左旋平衡处理 */
	}
}

/*  若在平衡的二叉排序树T中不存在和e有同样关键字的结点,则插入一个 */
/*  数据元素为e的新结点。并返回1,否则返回0。若因插入而使二叉排序树 */
/*  失去平衡,则作平衡旋转处理。布尔变量taller反映T长高与否。 */
Status InsertAVL(BiTree *T, int e, Status *taller)
{
	if (!*T)
	{ /*  插入新结点。树“长高”,置taller为TRUE */
		*T = (BiTree)malloc(sizeof(BiTNode));
		(*T)->data = e; (*T)->lchild = (*T)->rchild = NULL; (*T)->bf = EH;
		*taller = TRUE;
	}
	else
	{
		if (e == (*T)->data)
		{ /*  树中已存在和e有同样关键字的结点则不再插入 */
			*taller = FALSE; return FALSE;
		}
		if (e < (*T)->data)
		{ /*  应继续在T的左子树中进行搜索 */
			if (!InsertAVL(&(*T)->lchild, e, taller)) /*  未插入 */
				return FALSE;
			if (*taller) /*   已插入到T的左子树中且左子树“长高” */
				switch ((*T)->bf) /*  检查T的平衡度 */
				{
				case LH: /*  原本左子树比右子树高。须要作左平衡处理 */
					LeftBalance(T); *taller = FALSE; break;
				case EH: /*  原本左、右子树等高,现因左子树增高而使树增高 */
					(*T)->bf = LH; *taller = TRUE; break;
				case RH: /*  原本右子树比左子树高,现左、右子树等高 */
					(*T)->bf = EH; *taller = FALSE; break;
				}
		}
		else
		{ /*  应继续在T的右子树中进行搜索 */
			if (!InsertAVL(&(*T)->rchild, e, taller)) /*  未插入 */
				return FALSE;
			if (*taller) /*  已插入到T的右子树且右子树“长高” */
				switch ((*T)->bf) /*  检查T的平衡度 */
				{
				case LH: /*  原本左子树比右子树高。现左、右子树等高 */
					(*T)->bf = EH; *taller = FALSE; break;
				case EH: /*  原本左、右子树等高,现因右子树增高而使树增高  */
					(*T)->bf = RH; *taller = TRUE; break;
				case RH: /*  原本右子树比左子树高,须要作右平衡处理 */
					RightBalance(T); *taller = FALSE; break;
				}
		}
	}
	return TRUE;
}

int main(void)
{
	int i;
	int a[10] = { 30,22,34,7,36,28,29,26,24};
	BiTree T = NULL;
	Status taller;
	for (i = 0; i < 10; i++)
	{
		InsertAVL(&T, a[i], &taller);
	}
	printf("本例子建议断点跟踪查看平衡二叉树结构");
	return 0;
}

1.AVL插入代码(InsertAVL)

注意1:*taller是个指针,一处改,处处改;

因为上层树是否长高取决于下层树是否长高,所以下层树若长高;

比如:如果下层某树结点C仅仅从RH(插入左结点后)变为了EH(没长高),那么此时就会令:*taller=FALSE;

那么因为是递归调用,故C的上层所有树在接收递归返回taller时,接收的全为FALSE,也就是说:底层树没长高,那么依赖于此底层树的上层树全部不会长高!

底层树C从RH变为了EH,C没长高,因此A/B的bf也不会变化

 

注意2:插入key时递归调用InsertAVL,层层返回,每回*T要注意是哪层;

因为是递归调用,每次发现比现有值大(小)时,就递归调用InsertAVL进入其右(左)子树,直到:

①进入不存在的叶子节点执行插入操作

②找到已有值与插入值相等,返回FALSE,FALSE

执行插入操作后,需要层层返回,每返回上一层:

对该层原先的bf进行检查并修改(因插入一个New结点带来的变化)->并决定该层是否长高,再返回到上上一层

 

如此循环!

 

注意3:调用左右平衡函数时,bf值一定是已经更新后的值

因为是递归调用,所以插入一个结点后变的不平衡,需要调用左平衡处理函数或右平衡处理函数时,一定是:

①对其进行操作的一定是最小不平衡树(因为是递归调用,层层返回,若发现不平衡需要调用左右平衡函数时,一定是从下往上刚刚发现的,也就是最小的那个不平衡树)

②该不平衡树下的每个结点的bf值都已经更新为插入New结点后的bf值(因为递归调用到不平衡树根节点时,其下层一定递归调用完了,且进行了bf检查和修改)


2.左平衡处理函数(LeftBalance)

根据前面的代码,可以知道以下问题:

Q1:什么时候会调用左子树?

A1:调用左平衡处理的T结点之前就是LH,现在又往其左子树的下方(左或右都可以)插入了一个New结点导致左子树长高了(返回*taller=true),从而T结点子树也长高了(上层树长高取决于下层树),T本来就是LH,它的左结点又长高了,那肯定不平衡了,故需要调用左平衡处理函数。

 

Q2:调用左平衡处理函数一定是New结点插入在左边吗?

A2:当然不是,调用左平衡处理函数的本质是因为T的左结点L长高了,T的左结点L长高可以是插入L的右结点让它长高,也可以插入左结点让它长高

      只要是L长高了,不论哪侧高,都是T的左结点(左侧)高了,因此T的bf都会左边加一(左高了)

插入以下右结点也会调用LeftBalance函数:

插入右结点此时会调用:左平衡处理函数

 

插入右结点此时会调用:左平衡处理函数

 

Q3:调用左平衡处理函数传入的不平衡树各结点的bf值是插入New结点后的更新值,对吗?

A3:,因为我们是递归调用,当需要调用左平衡处理函数时,说明已经递归返回到了T结点,因此T结点以下的结点的bf值全部都在递归返回时已经检查并作出更新了。

      只要是L长高了,不论哪侧高,都是T的左结点(左侧)高了,因此T的bf都会左边加一(左高了)

明白了以上三个问题,AVL树的左平衡处理函数的理解简直不要太简单!

情况一:插入New结点后,T的左结点L(T->lchild)为LH,从而导致T不平衡

即:

 

Figure1:LH=1
Figure2:LH=1

可以看出,不论是哪种情况,只要是其T的左子树LH=1,其本质都是LL型,因此,一个单右旋操作即可重新恢复平衡性。

对应代码:

//L=(*T)->lchild;        //将T的左子树赋为L
//LH=1,RH=-1,EH=0;       //三种平衡因子bf

//检查T的左子树bf值,若为1,则做单右旋处理
switch(L->bf)
{
	//T->lchild->bf若为1,则插入到了T的左子树的左子树
	//本质为LL型,因此做单右旋处理
	case LH:
		R_Rotate(T);			//单右旋处理
		(*T)->bf=L->bf=EH;		//恢复相应的bf值
		break;
}

情况二:T的左子树平衡因子是RH

插入New结点后,T的左子树L的平衡因子是RH,那么其New结点一定是插入到了L的右侧。

那么:新的New结点插入其本质是LR型插入,需要进行双旋处理

双旋处理后,我们需要恢复相对应的bf值,令Lr=L->rchild,因此又可细分为:

①Lr->bf=LH;

插入New结点后,Lr->bf==LH

进行双旋调整后,相应的:

T->bf为RH;

L->bf为EH;

Lr->bf为RH;

Lr->bf为LH时,经过双旋调整后T,L,Lr的bf值变化情况

对应代码:

//检查T的左子树bf值,若为-1,则做双旋处理
switch(L->bf)
{
	case RH:
		Lr=L->rchild;			        //L的右子树赋为Lr
		
		//检查Lr的bf情况,作相应的bf恢复调整
		switch(Lr->bf)
		{
			case LH:
				(*T)->bf=RH;	        //调整T的bf值
				L->bf=EH;		//调整L的bf值
		}	
		Lr->bf =EH;				//调整Lr的bf值
		L_Rotate(&(*T)->lchild);                //双旋第一旋:对T的左子树左旋处理
		R_Rotate(T);			        //双旋第二旋:对T进行右旋处理
}

 

②Lr->bf=RH;

插入New结点后,Lr->bf==RH

对应代码:

//检查T的左子树bf值,若为-1,则做双旋处理
switch(L->bf)
{
	case RH:
		Lr=L->rchild;			            //L的右子树赋为Lr
		//检查Lr的bf情况,作相应的bf恢复调整
		switch(Lr->bf)
		{
			case RH:
				L->bf =LH;		    //调整T的bf值
				(*T)->bf=EH;		    //调整L的bf值
				break;
		}	
		Lr->bf =EH;				    //调整Lr的bf值
		L_Rotate(&(*T)->lchild);	            //双旋第一旋:对T的左子树左旋处理
		R_Rotate(T);				    //双旋第二旋:对T进行右旋处理
}

③Lr->bf=EH;

插入New结点后,Lr->bf==EH

对应代码:

//检查T的左子树bf值,若为-1,则做双旋处理
switch(L->bf)
{

	case RH:
		Lr=L->rchild;			        //L的右子树赋为Lr
		//检查Lr的bf情况,作相应的bf恢复调整
		switch(Lr->bf)
		{
			case EH:
				L->bf=(*T)->bf=EH;	//调整T、L的bf值
				break;	
		}	
		Lr->bf =EH;				//调整Lr的bf值
		L_Rotate(&(*T)->lchild);	        //双旋第一旋:对T的左子树左旋处理
		R_Rotate(T);				//双旋第二旋:对T进行右旋处理
}

 

 

完整代码:

//检查T的左子树bf值,若为1,则进行单右旋处理;若为-1,则做双旋处理;
switch(L->bf)
{
	//T->lchild->bf若为1,则插入到了T的左子树的左子树
	//本质为LL型,因此做单右旋处理
	case LH:
		R_Rotate(T);			        //单右旋处理
		(*T)->bf=L->bf=EH;		        //恢复相应的bf值
		break;

	//T->lchild->bf若为-1,则插入到了T的左子树的右侧
	//本质为LR型,因此做双旋处理
	case RH:
		Lr=L->rchild;			        //L的右子树赋为Lr
		//检查Lr的bf情况,作相应的bf恢复调整
		switch(Lr->bf)
		{
			case LH:
				(*T)->bf=RH;	        //调整T的bf值
				L->bf=EH;		//调整L的bf值
				break;
			case RH:
				L->bf =LH;
				(*T)->bf=EH;
				break;
			case EH:
				L->bf=(*T)->bf=EH;
				break;	
		}	
		Lr->bf =EH;				//调整Lr的bf值
		L_Rotate(&(*T)->lchild);                //双旋第一旋:对T的左子树左旋处理
		R_Rotate(T);			        //双旋第二旋:对T进行右旋处理
}

 

 

 

 

  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值