C数据结构与算法-基础整理-树-09:平衡二叉树(AVL树)

0x01.平衡二叉树的由来

问题:二叉排序树广泛应用于动态查找中(查找的同时会往动态表里增删数据),优势就在于增删查找数据的时候,时间代价比线性表小很多,但二叉排序树的插入操作中,也可能创建了一棵这样的树:

在是一棵左斜树,也完全符合二叉排序树的特征,但这样的排序树是没有意义的,因为它失去了减少时间代价的意义,就和一个普通的线性表一样,发挥不出树的优势了。

解决办法:对插入的结点进行规范,时刻调整这棵树,使之平衡,也就是让它变成一棵平衡的排序二叉树。

0x02.平衡二叉树的相关概念

平衡二叉树(也叫AVL树),是一棵高度平衡的二叉排序树,需要满足以下的特征:

  • 是一棵二叉排序树
  • 每一个结点的左子树和右子树的高度差至多等于1

平衡因子BF:二叉树上结点的左子树的深度减去右子树的深度的值称为平衡因子BF。平衡二叉树的BF只可能是1,0,-1。

最小不平衡子树:距离插入点最结点最近的,且平衡因子的绝对值大于1的结点为根的子树。

如图,以48为根的子树j就是最小不平衡子树。

0x03.如何让二叉排序树平衡

最简单的开始,这棵树怎样才能平衡呢?

很明显,48这个结点为根的左子树 深度是2,右子树深度是0,BF值是2,不平衡,这时只需要将48变成43的子树,这棵树就平衡了。

再如下:

这只需要将37变为43的左子树就行了。

其实第一种操作叫右璇操作,原因就是左边的多了,要放一些到右边去,再来看一个复杂一点的右旋操作。

此时根结点需要右璇,应该将45作为根结点,48作为45的右孩子,45原来的右孩子47变成48的左孩子,50还是48的右孩子。

到这我们可以理解右璇的基本操作原理:将原来根结点的左孩子作为根结点,原来根结点的左孩子变为新的根结点的右孩子,原来根结点的右孩子不变。

我们再来看一下复杂一点的左璇操作:

这时,应该将47作为新的根结点,39作为47的左孩子,45变为39的右孩子。

我们再来看一个特殊的例子:

此时有两个结点不平衡,47和48,48的BF为-2,最小不平衡子树是以48为根结点,47的BF也为-2,50的BF为1,此时按道理只需要旋转最下不平衡子树就行了,也就是48,50,49这棵子树,但如果左璇,会发现下图:

此时这明显不满足二叉排序树的要求,因为,48<50,但49却是50的右子树。

我们观察根本原因在于,50的BF值为1,而48的BF值为-2,而在上述的左璇中,最小不平衡子树的BF值与子结点的BF值相同,所以我们需要使它的结点的BF值相同再操作。调整50,49的BF值为负值,也就是将50,49先右璇,如图:

此时再对48,49,50进行右璇就可以得到平衡的二叉树:

通过上述的调整,我们发现,只要出现了不平衡,那么先找最小不平衡子树的根结点,再对这个结点进行相应的旋转,就能使树平衡。

0x04.代码

首先我们要看一下平衡二叉树的结构,与普通二叉树不同的是,多出了一个bf值,用于判断是否平衡。

//定义左高,右高,等高的 值
#define LH 1
#define RH -1
#define EH 0
typedef struct TreeNode
{
	int data;
	int bf;
	struct TreeNode* Left;
	struct TreeNode* Right;
}TreeNode, * BinTree;

然后,我们需要写出操作二叉树使之平衡的最基本的代码,也就是,左璇和右璇,根据上述的原理,我们可以得出下面的代码:

右璇:

//传入需要做右璇处理的子树的根结点
//处理完毕后P指向新的子树的根结点
void R_Rotate(BinTree* P)
{
	BinTree L;
	L = (*P)->Left;
	(*P)->Left = L->Right;
	L->Right=(*P);
	*P = L;
}

左璇:

void L_Rotate(BinTree* P)
{
	BinTree R;
	R = (*P)->Right;
	(*P)->Right = R->Left;
	R->Left = (*P);
	*P = R;
}

有了最基本的左璇和右璇代码后,需要处理平衡左子数和平衡右子树的代码:

左子树平衡旋转代码:

//传进来需要左平衡处理的结点,结束后,T指向新的子树的根结点
//既然已经需要做左平衡处理,那么它的BF值肯定为正,只要判断它的左子树的BF值进行了
void LeftBalance(BinTree* T)
{
	BinTree L, Lr;
	L = (*T)->Left;
	switch (L->bf)//检查左子树的平衡度
	{
	case LH://如果左边高,那么BF值都为正,只需要做右璇处理就行了
		(*T)->bf = L->bf = EH;//处理完毕后,BF值都为0
		R_Rotate(T);
		break;
	case RH://如果左子树的BF值为负,说明符号为负,需要先把符号变为相同,再进行右璇处理
		Lr = L->Right;
		//后面会进行双旋操作
		switch (Lr->bf)//通过判断Lr的bf值,修改T和T的左孩子的平衡因子
		{
		case LH://如果L的右子树是左高,那么双旋过后,T应该就是右高
			(*T)->bf = RH;
			L->bf = EH;
			break;
		case EH://如果等高,那么双旋过后应该是都是等高
			(*T)->bf = L->bf = EH;
			break;
		case RH://如果是右高,那么双旋过后,L应该是左高
			(*T)->bf = EH;
			L->bf = LH;	
			break;
		}
		Lr -> bf = EH;
		L_Rotate(&(*T)->Left);//对T的左子树进行左旋
		R_Rotate(T);//对T进行右旋,整个是个双旋操作
	}
}

同理可得右平衡代码

右子树平衡旋转代码:

void RightBalance(BinTree* T)
{
	BinTree R, Rl;
	R = (*T)->Right;
	switch (R -> bf)
	{
	case RH:
		(*T)->bf = R->bf = EH;
		break;
	case LH:
		Rl = R->Left;
		switch (Rl->bf)
		{
		case LH:
			(*T)->bf = EH;
			R->bf = RH;
		case EH:
			(*T)->bf = R->bf = EH;
			break;
		case RH:
			(*T)->bf = LH;
			R->bf = EH;
		}
		Rl->bf = EH;
		R_Rotate(&(*T)->Right);
		L_Rotate(T);
	}
}

有了这些平衡操作,我们就可以将二叉排序树的插入过程改为生成AVL树的过程了。

AVL树插入过程代码:

//若在二叉排序树中不存在关键字为e的结点,则将e插入
//插入的过程通过旋转操作保持树的平衡
//插入成功返回true,否则返回false
//taller是一个判断二叉树的高度是否增加的值
int InsertAVL(BinTree* T, int e, int* taller)
{
	if (!*T)//找到位置,插入结点
	{
		*T = (BinTree)malloc(sizeof(TreeNode));
		(*T)->data = e;
		(*T)->Left = (*T)->Right = NULL;
		(*T)->bf = EH;
		*taller = true;
	}
	else
	{
		if (e == (*T)->data)//树中有关键字和e相同的结点,不进行插入操作
		{
			*taller = false;
			return false;
		}
		if (e < (*T)->data)//在左子树中进行搜索
		{
			if (!InsertAVL(&(*T)->Left, e, taller))//在左子树中递归,若最终返回false,则该步返回false
			{
				return false;
			}
			if (*taller)//这个结点插入到了左子树(到这步的时候,实际上已经从InsertAVL返回了)
			{
				switch ((*T)->bf)//判断T的平衡因子,这个T的bf值仍然是插入操作之前的值
				{
				case LH://原来是左子树高,现在左子树又增加一个结点,左子树偏高,做左子树平衡处理
					LeftBalance(T);
					*taller = false;//平衡完后重置标志变量
					break;
				case EH://原来是等高,现在左子树加了,说明左子树高了
					(*T)->bf = LH;
					*taller = true;//加入后,仍然左高,那么高度是真的增加了
					break;
				case RH://原来右边高,现在左边增加了一个结点,等高了
					(*T)->bf = EH;
					*taller = false;//因为是等高,所以重置标志变量
					break;
				}
			}
		}
		else//在右子树中搜素,过程与上面差不多
		{
			if (!InsertAVL(&(*T)->Right, e, taller))
			{
				return false;
			}
			if (*taller)
			{
				switch ((*T)->bf)
				{
				case LH://原来左边高,现在等高
					(*T)->bf = EH;
					*taller = false;
					break;
				case EH://原来等高,现在右边高
					(*T)->bf = RH;
					*taller = true;
					break;
				case RH://原来右边高,现在又加了一个,需要左右平衡处理
					RightBalance(T);
					*taller = false;
					break;
				}
			}
		}
	}
}

0x05.总结

平衡二叉树对树进行了高度的旋转处理,保证了平衡性,对动态查找非常有好处。

本章结束。

发布了50 篇原创文章 · 获赞 35 · 访问量 1309

猜你喜欢

转载自blog.csdn.net/ATFWUS/article/details/104429717