平衡二叉树(AVL树)学习笔记-C语言,附手绘过程

平衡二叉树(AVL树)学习笔记-C语言,附手绘过程,第1张

        最近啃了一下排序算法中的平衡二叉树(AVL树),经过一晚上苦逼种树之后,总算是对平衡二叉树的插入与调整算法有了一定认识。(本文暂时没有关于删除 *** 作的内容,至于为什么,因为我看的那本书上没讲。。。)

        关于平衡二叉树的定义,可以参考以下文章:

平衡二叉树详解 通俗易懂https://blog.csdn.net/jarvan5/article/details/112428036?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166320624416800186513373%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166320624416800186513373&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-112428036-null-null.142^v47^pc_rank_34_default_2,201^v3^control_2&utm_term=%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91&spm=1018.2226.3001.4187       

         在感受了左旋/右旋 *** 作的魅力之后,我开始尝试着自己编写一个平衡二叉树的初始化与插入算法。定义节点结构如下:

typedef struct Node
{
	int data;    //数据域
	int bf;      //平衡因子 balance factor
	struct Node* lchild, * rchild;
} Node, * TreeNode;

        根据平衡二叉树的定义可知,平衡因子bf的取值只能是-1,0, 1三者之一,如果出现了|bf|>1的情况,则说明树的结构出现了不平衡,需要进行调整。但这种调整并不一定是针对整棵树,只需要对最小不平衡子树进行调整即可。

所谓的最小不平衡子树,是指“离插入位置最近,且满足|bf|>1的节点作为根节点的树”,从数值角度来理解,也就是找到整棵树中唯一一个自身的|bf|>1,且左右子节点(如果存在的话)的|bf|=1/0的节点作为根节点。

         对于最小不平衡子树的查找,应该放在什么时候呢?我最初的想法是,等当前的插入过程全部结束,即递归返回根节点之后,再从根节点开始,对整棵树进行搜索,但是经过思考我便发现,最小不平衡子树的根节点,必然会落在之前插入过程中所访问过的某个节点上!也就是说,在插入成功进行递归返回时,便可以顺道寻找满足上述条件的节点。于是我构想了以下这样的算法:

enum Status { NO, YES };

int AVL_insert(TreeNode *T, int value)
{

	if (value > (*T)->data)    //向右子树中进行查找或插入
	{
		if ((*T)->rchild == NULL)
		{
			TreeNode new_node = (TreeNode)malloc(sizeof(Node));
			new_node->lchild = new_node->rchild = NULL;
			new_node->data = value;
			new_node->bf = 0;
			(*T)->rchild = new_node;
			if ((*T)->lchild == NULL)
			{
				(*T)->bf -= 1;
				return YES;
			}
			else
				return NO;
		}
		else
		{
			if (AVL_insert(&(*T)->rchild, value))
			{
				(*T)->bf -= 1;
				if ((*T)->bf <= -2)
				{
					AVL_adjust(T);
					return NO;
				}
				return YES;
			}
		}
	}

	else    //向左子树中进行查找或插入
	{
		if ((*T)->lchild == NULL)
		{
			TreeNode new_node = (TreeNode)malloc(sizeof(Node));
			new_node->lchild = new_node->rchild = NULL;
			new_node->data = value;
			new_node->bf = 0;
			(*T)->lchild = new_node;
			if ((*T)->rchild == NULL)
			{
				(*T)->bf += 1;
				return YES;
			}
			else
				return NO;
		}
		else
		{
			if (AVL_insert(&(*T)->lchild, value))
			{
				(*T)->bf += 1;
				if ((*T)->bf >= 2)
				{
					AVL_adjust(T);
					return NO;
				}
				return YES;
			}
		}
	}
}

        在以上代码中,我设置了标志当前插入过程返回结果的枚举量Status,NO=0表示当前完成返回的这一次插入 *** 作没有对根节点T的bf值造成影响(也就是树的层数没有增高),而YES=1则表示当前返回的插入 *** 作改变了根节点T的bf值。由于所有的插入过程都只可能对插入节点的直系亲属(也就是爸爸、爸爸的爸爸、……)的bf值造成影响(这个应该还是比较容易理解的),因此在每次递归返回时对当前根节点的bf值进行判别和修改,就能够完成对整棵树中所需要的全部bf值更新 *** 作。

        下面我们考虑在什么情况下需要对树的平衡性进行调整。平衡二叉树的理念是“时刻维护一棵平衡二叉树”,也就是说,不论在什么时候,只要树中出现了任何不平衡的因素(具体表现为|bf|>1),我们都要进行调整,使其平衡之后,再进行后续 *** 作。经前述可知,某一次插入过程中,不平衡现象只可能发生在递归返回过程中对根节点的bf值进行修改时,因此,每次对根节点的bf值进行调整之后(也就是AVL_insert函数返回YES后),都需要判断当前节点的bf值是否已经超出了范围:

if (AVL_insert(&(*T)->rchild, value))
{
	(*T)->bf -= 1;
	if ((*T)->bf <= -2)
	{
		AVL_adjust(T);
		return NO;
	}
	return YES;
}

        因为我自己写的时候没有看书上的方法,因此在AVL_insert()函数的实现细节上,与书上介绍的方法还是有比较大的区别的。书上的做法是逐节点比较,直到当前调用的根节点为NULL时,才进行插入 *** 作,通过一个叫做taller的变量来判断子树是否长高,函数自身返回值被用来判断是否插入成功了(即有没有重复项)。我写的时候没有考虑重复项问题(其实考虑了最多也就是设置一个表示有重复项的flag),而且正好返回值可以表明子树是否长高,就直接拿来用了,效果好像还不错。

关于一些细节问题的解释:

1.如何判断插入节点是否会引起高度变化:

相信下面这这张图已经足够说明问题了,关键就是在插入时判断另一侧有无兄弟节点,这也是在根节点的子树而不是空节点上插入带来的方便之处。

2.为什么可以直接将“>2”和“<-2”分开判断: 

在左子树上进行添加 *** 作,也就是通过value < (*T)->data这一条件来调用AVL_insert()时,如果返回了YES,只有可能是左子树增高,也即bf=左高-右高会增大;同理,在右子树上 *** 作时,只有可能是右子树增高。因此若要产生不平衡,只有可能是左子树增高后bf>2,或右子树增高后bf<-2。

        在弄清了应该在何时调整树的结构后,接下来要关注的就是怎样进行调整。通常的教材中会大致介绍一遍四种旋转方法——RR、LL、RL和LR。

        下面先从比较简单的RR和LL两种旋转方法入手。

         RR是由于不断往右子树中添加节点而造成|bf|=-2的情况,就最简单的三节点情况来说,需要把最小不平衡子树根节点的右子树替换到根节点的位置,而原本的根节点则作为其左子树,看起来就像把整棵子树向左旋转了一样,故称这种 *** 作为“左旋”。在一般情况中,还可能有更多的下层结构,在旋转时,需要将最小不平衡子树根节点的右子树的左子树(也就是图中的 2,这个名字有点长。。。)重接为原根节点的右子树,以保证原本树中节点的完整。类似的,LL也是同样的 *** 作,只不过改成了“右旋”。

        具体代码实现如下,其中T是最小不平衡子树的根节点,L/R分别是其左、右子树:

void R_rotate(TreeNode *T)    //右旋 *** 作
{
	TreeNode L = (*T)->lchild;
	(*T)->lchild = L->rchild;
	L->rchild = (*T);
	*T = L;
}

void L_rotate(TreeNode *T)    //左旋 *** 作
{
	TreeNode R = (*T)->rchild;
	(*T)->rchild = R->lchild;
	R->lchild = (*T);
	*T = R;
}

        顺带一提,在进行左旋/右旋后,只有两个节点的bf值会发生改变,也就是根节点T、左子节点L或右子节点R,而且在两种旋转方式中,从图例可以看出旋转后两个节点的bf值都变成了0。(为之后埋伏笔);另外LL和RR两种旋转方式的判定条件为T与R/L节点的bf值同号(即图示的+2/+1或-2/-1)

        以上两种 *** 作结合图例说明应该还是比较好理解的,但是到了LR和RL,事情似乎就没有那么简单了。先来看看LR:

        乍看之下,LR相比于之前的LL和RR,要多出一步旋转 *** 作,但这么做的目的是什么呢?让我们从一般情况出发:

         从图中可以看出,LR相较于LL,区别在于L节点上的bf值由+1变为了-1(注意L的bf值必不能为0!至于为什么不能是0,可以从“时刻维护一棵平衡二叉树”这一思想的角度证明,暂且按下不表(PS.或许会有附录什么的吧)),这就意味着L其实是“左轻右重”的。但根节点T却是“左重右轻”,如果直接像LL一样进行右旋的话,会使得旋转后的新根节点(L)的bf变成-2,无法达到使树平衡的目的。参考LL的经验,如果能将L的bf值转化为正值(“左重右轻”),则在旋转后可以避免上述问题。因此,我们先对以L为根节点的子树进行考虑:

        如果要将本来bf<0的L节点子树变为bf>0,则需要进行左旋 *** 作,让L的左子树层级增高,右子树层级减少。这里需要根据L节点右子节点Lr的bf值,分以下几类情况来讨论:

        其中第三种情况正好对应之前提到的简单情况,个人感觉考虑的时候很容易忽略掉这个(至少我自己就忽略了。。。)根据Lr节点的bf值不同,初步旋转得到的L子树也存在一些差异。值得注意的是在情况①中,可能会出现局部bf值为±2的情况,这种情况在调整过程中是允许出现的,因为从之后的再次旋转 *** 作可以看到,最终+2的bf值被调整为了0。

        通过这幅图,我相信对于下面代码的逻辑,你们应该能够一目了然了:

TreeNode lrchild = (*T)->lchild->rchild;
switch (lrchild->bf)
{
    case 1:
	    (*T)->bf = -1;
	    (*T)->lchild->bf = lrchild->bf = 0;
	    break;
	case 0:
		(*T)->bf = (*T)->lchild->bf = lrchild->bf = 0;
		break;
	case -1:
		(*T)->lchild->bf = 1;
		(*T)->bf = lrchild->bf = 0;
		break;
}
L_rotate(&(*T)->lchild);
R_rotate(T);

        说句题外话,在我看书上的代码的时候,他对于switch语句中那些case0、case1、case2表示什么意思全都语焉不详,而我一开始自己写的时候又没考虑Lr的bf=0的情况,费了好大劲才看懂为什么会有case0这个分支。这段代码没有解释的时候看起来很神奇,自己试过之后才发现其实是暴力枚举出来的。。。

        RL的情况与LR大致相同,只是左右方向相反,具体看下图:

         此时需要对R节点的左子节点Rl的bf值进行分类讨论,也可以枚举出三种可能情况。细节就不再赘述了。

        对树进行调整平衡的函数代码如下,主要分RR、LL、RL和LR四种情况,分别进行处理:

void AVL_adjust(TreeNode *T)
{
	/*调整最小不平衡子树*/
	if ((*T)->bf == 2)
	{
		if ((*T)->lchild->bf == 1)
		{
			/*LL型,只需要将左子树右旋*/
			(*T)->bf = (*T)->lchild->bf = 0;
			R_rotate(T);
		}
		else
		{
			/*LR型,需要对左子树先进行左旋调整*/
			TreeNode lrchild = (*T)->lchild->rchild;
			switch (lrchild->bf)
			{
			case 1:
				(*T)->bf = -1;
				(*T)->lchild->bf = lrchild->bf = 0;
				break;
			case 0:
				(*T)->bf = (*T)->lchild->bf = lrchild->bf = 0;
				break;
			case -1:
				(*T)->lchild->bf = 1;
				(*T)->bf = lrchild->bf = 0;
				break;
			}
			L_rotate(&(*T)->lchild);
			R_rotate(T);
		}
	}
	else
	{
		if ((*T)->rchild->bf == -1)
		{
			/*RR型,只需要将左子树左旋*/
			(*T)->bf = (*T)->rchild->bf = 0;
			L_rotate(T);
		}
		else
		{
			/*RL型,需要对左子树先进行右旋调整*/
			TreeNode rlchild = (*T)->rchild->lchild;
			switch (rlchild->bf)
			{
			case 1:
				(*T)->rchild->bf = -1;
				(*T)->bf = rlchild->bf = 0;
				break;
			case 0:
				(*T)->bf = (*T)->rchild->bf = rlchild->bf = 0;
				break;
			case -1:
				(*T)->bf = 1;
				(*T)->rchild->bf = rlchild->bf = 0;
				break;
			}
			R_rotate(&(*T)->rchild);
			L_rotate(T);
		}
	}
}

         在具体使用的时候,只需要调用前面的AVL_insert()函数进行元素插入即可,调节平衡的 *** 作会在插入过程中自动判断并实现。


        最后,肝文不易,如果觉得对你有帮助的话,还请看到这里的各位读者动动手指点个赞或者收藏支持一下,谢谢啦!当然也欢迎关注哦!(虽然更新比较看心情……)

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/2990396.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-23
下一篇 2022-09-23

发表评论

登录后才能评论

评论列表(0条)

保存