平衡二叉树(AVL树) ----- C语言

前面说到了二叉搜索树,它在动态查找表中有较高的性能,既能保证在无序数据中查找的高效率,又相对于数组具有方便的增添删除功能,

其平均查找复杂度为所构造的二叉树的树高,即O(logn)( 图1-1 ),但是,对于极端情况,例如该二叉树是一颗左斜树或者右斜树 (图1-2) 。

此时,显然可以看到,最坏查找时间复杂度为O(n),这就意味着我们的二叉搜索树变成了顺序查找,效率极为低下,所以我们需要一种

平衡二叉树的方法来避免出现极端情况,方法有很多,典型的有AVL树,红黑树等,他们各有优势,这里说的是AVL树。(注:平衡二叉树

中每一个节点的左子树和右子树高度差至多等于 1 )

《平衡二叉树(AVL树) ----- C语言》《平衡二叉树(AVL树) ----- C语言》

        图1-1 图1-2

目前网上有两种AVL树的平衡方式,一种是树高度法,另外一种是平衡因子法,据说树高度法存在效率问题,本人目前才疏学浅,对树高度法了解

甚少,故对网上的评论不置评论,这里采用的是平衡因子的方法。既然有一个因子用来作为二叉树的平衡判断方式,那么我们的二叉树结构需作

相应修改 (注:平衡因子指当前节点的左子树高度减去右子树高度的差) 

typedef struct Anode{
	int data;
	int bf;//即当前节点的左子树高度减去右子树高度的差
	struct Anode *lchild, *rchild;
}Anode, *AVLTree;/*AVL树结构,在二叉树基础上加了一个bf平衡因子*/

AVL树的关键字查找操作与二叉搜索树类似,就不再赘述,后面直接上代码,这里主要是插入和删除操作需要做大调整。我们知道,给你一堆数据,

通过循环调用二叉搜索树的插入代码,就可以完成一颗二叉搜索树的构建,但是若要达到平衡效果,就要时时刻刻监测当前插入的节点是否破坏了

二叉搜索树的平衡性,正所谓 “把XXX扼杀在摇篮里” ,每次插入一个新节点,若破坏平衡性,就调整它,若删除一个节点破坏了树的平衡,也如此。

首先让我们看看破坏平衡的四种情况,用构建一颗AVL树来说明,假如我们给定a[10] = {3,2,1,4,5,6,7,10,9,8}来构建二叉搜索树(注:图中节点左上角

数字表示当前节点的 bf 平衡因子值) 

《平衡二叉树(AVL树) ----- C语言》《平衡二叉树(AVL树) ----- C语言》

      图1 图2 图3    图4


一、在构建二叉搜索树过程中,图1图2 是典型的最小不平衡子树右旋过程及结果,当我们插入 节点1 时,可以知道 节点3  的平衡因子变为了 ,因此需要调整,我们将从 节点3 开始的最小不平衡子树右旋 (顺时针),此时 节点2 变成了树的根节点,整棵树达到平衡状态;

二、图3 图4 是最小不平衡子树左旋过程及结果,当插入 节点5 后, 节点2 和 节点3 平衡因子都变成了 –2,但是最小不平衡子树根节点是离插入节点最近的不平衡节点,所以此时的最小不平衡子树是以 节点3 为根节点的子树,将整棵树左旋 (逆时针),此时 节点4 成为新的子树根节点,子树达到平衡状态;


《平衡二叉树(AVL树) ----- C语言》        《平衡二叉树(AVL树) ----- C语言》

  图5     图6

《平衡二叉树(AVL树) ----- C语言》

           图7


三、图5、图6 跟 图7 是最小不平衡子树的双旋情况,图5若根据前面讲解左旋后 节点9 变成了 节点10 的右孩子,这就不满足二叉搜索树的定义了,故需要调整,仔细观察发现最小不平衡子树根 节点7 与其右孩子 节点10 平衡因子一正一负,而我们之前说的左旋跟右旋,最小不平衡子树根节点的平衡因子与其孩子是同符号的,所以我们可以先把以 节点10 为根节点的子树进行右旋,形成 图6 ,此时再左旋以 节点7 为根节点的子树,达到平衡状态;(注:此处节点9并无右孩子,否则应该在 节点10 右旋后作为节点10 的左孩子,而节点9 变成子树新根节点,如此才能达到平衡且与节点7  同符号以进行后续的节点7 左旋 )

《平衡二叉树(AVL树) ----- C语言》《平衡二叉树(AVL树) ----- C语言》

        图8  图9

《平衡二叉树(AVL树) ----- C语言》

  图10


四、图8、图9 跟 图10 是最小不平衡子树双旋的另一种情况,同样当我们插入 节点8 之后,最小不平衡子树的根 节点6 平衡因子变为了 -2 ,此时应该左旋,但是其右孩子 节点9 的平衡因子与其不同符号,故我们需要先将以 节点9 为根节点的子树进行右旋,得到 图9 ,注意此时右旋由于 节点7 将作为新的子树根节点而原来 节点7 的右孩子 节点8 将划分为 节点9 的左孩子,这样才能达到平衡的效果 (可参考下图) ,之后再将以 节点6 为根节点的最小不平衡子树进行左旋,得到 图10 ,达到平衡效果。

《平衡二叉树(AVL树) ----- C语言》

说了很多,终于要上代码了,好累,解释可能还不够清楚,大家可以多多参考其他数据结构相关的书 ——- 

/*	
*	平衡二叉树(AVL):由于在极端情况下(左斜树和右斜树)
*	二叉搜索树(BST)查找时间效率大大降低到O(n),故需要
*	对其进行平衡,平衡的方法有很多种,如AVL,红黑树等,
*	此处通过平衡因子的方法使用AVL树来达到平衡的目的。
*
*	2015.12.06
*	By Snow Yong
*
*/


#define LH -1		//LH,EH,RH表示平衡因子值
#define EH 0
#define RH 1
#include <stdio.h>
#include <stdlib.h>

typedef struct Anode{
	int data;
	int bf;//即当前节点的左子树高度减去右子树高度的差
	struct Anode *lchild, *rchild;
}Anode, *AVLTree;/*AVL树结构,在二叉树基础上加了一个bf平衡因子*/


int InsertAVL(AVLTree *T, int key, int *taller);
int LeftBalance(AVLTree *T);
int RightBalance(AVLTree *T);
void LeftRotate(AVLTree *T);
void RightRotate(AVLTree *T);
int DeleteAVL(AVLTree *T, int key, int *lower);
int Delete(AVLTree *p);
int SearchAVL(AVLTree T, int key, AVLTree f, AVLTree *p);
void InOrderView(AVLTree T);



//AVL树插入关键字操作,*taller用来记录插入节点后树是否有长高
int InsertAVL(AVLTree *T, int key, int *taller)
{
	if (!(*T))
	{//若AVL树是空树,则直接分配节点内存存入关键字,树长高
		(*T) = (AVLTree)malloc(sizeof(Anode));
		(*T)->data = key;
		(*T)->lchild = (*T)->rchild = NULL;
		(*taller) = 1;
		return 1;
	}
	else
	{//若AVL非空树
		if (key == (*T)->data)
		{//如果找到当前节点的值等于欲插入的关键字,则说明已存在该关键字,不再重复插入,树没长高
			(*taller) = 0;
			return 0;
		}
		else if (key < (*T)->data)
		{//若欲插入的关键字小于当前节点值,则在当前节点的左子树递归看是否插入该节点
			if (!InsertAVL(&(*T)->lchild, key, taller))
			{//插入失败
				return 0;
			}
			if (*taller)
			{//若插入成功,则*taller必然等于1,此时只要根据当前节点的平衡因子来调整树达到平衡
				switch ((*T)->bf)
				{						//插入节点在当前节点的左子树的某处
					case LH:			//如果原来左子树高度就比右子树高1
						LeftBalance(T); //就调用左平衡函数
						(*taller) = 0;  //平衡过后,树相对于原来的树并无长高,故设置(*taller) = 0
						break;
					case EH:			//如果原来左子树跟右子树等高
						(*T)->bf = LH;	//则当前节点的平衡因子就改为LH(左边高1)
						(*taller) = 1;	//相比原树,树长高
						break;
					case RH:			//如果原来右子树高度就比左子树高1
						(*T)->bf = EH;	//则在左子树插入关键字节点后,当前节点平衡因子变为EH(左右等高)
						(*taller) = 0;	//相比原树,树并未长高
						break;
				}
			}
		}
		else
		{//若欲插入的关键字大于当前节点值,则在当前节点的右子树递归看是否插入该节点
		 //以下代码道理同上,只不过是在右子树上做文章,不再赘述
			if (!InsertAVL(&(*T)->rchild, key, taller))
			{
				return 0;
			}
			if (*taller)
			{
				switch ((*T)->bf)
				{
					case LH:
						(*T)->bf = EH;
						(*taller) = 0;
						break;
					case EH:
						(*T)->bf = RH;
						(*taller) = 1;
						break;
					case RH:
						RightBalance(T);
						(*taller) = 0;
						break;
				}
			}
		}
		
		return 1;
	}
}


//左平衡函数,调用此函数说明当前节点的子树已然左边不平衡
int LeftBalance(AVLTree *T)
{//此函数包含了左平衡的两种情况:1.根节点单右旋		2.根节点左孩子左旋然后根节点右旋
	AVLTree L, Lr;
	L = (*T)->lchild;		//当前树根节点的左孩子
	
	switch (L->bf)
	{
		case LH:			//若L左子树高,则直接根节点单右旋,并修正根节点与其左孩子的平衡因子值
			(*T)->bf = L->bf = EH;		//这两句代码不可调换
			RightRotate(T);				//因为右旋转函数会改变原来的T值(即当前树根节点发生变化)
			break;
		case RH:
			Lr = L->rchild;	//若L右子树高,则根据L的右孩子Lr的平衡因子值来修正根节点T与L的平衡因子值
			
			switch(Lr->bf)
			{//分情况修改平衡因子值,请自行画辅助图理解
				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的右孩子平衡因子修正为EH,因左右等高
			
			LeftRotate(&(*T)->lchild);		//先对根节点T的左孩子进行左旋
			RightRotate(T);					//再对根节点T进行右旋
			break;
	}
}

//右平衡函数,道理同上,代码差别如同对称成像,不再赘述,自行阅读
int RightBalance(AVLTree *T)
{
	AVLTree R, Rl;
	R = (*T)->rchild;
	
	switch (R->bf)
	{
		case LH:
			Rl = R->lchild;
			
			switch(Rl->bf)
			{
				case LH:
					(*T)->bf = EH;
					R->bf = RH;
					break;
				case EH:
					(*T)->bf = R->bf = EH;
					break;
				case RH:
					(*T)->bf = LH;
					R->bf = EH;
					break;
			}
			Rl->bf = EH;
			
			RightRotate(&(*T)->rchild);
			LeftRotate(T);
			break;
		case RH:
			(*T)->bf = R->bf = EH;
			LeftRotate(T);
			break;
	}
}


//左旋函数
void LeftRotate(AVLTree *T)
{
	AVLTree R;			
	R = (*T)->rchild;				//取当前树根节点的右孩子
	(*T)->rchild = R->lchild;		//将右孩子R的左孩子作为当前节点*T的右孩子
	R->lchild = (*T);				//将*T作为R的左孩子
	(*T) = R;						//将当前节点的右孩子R作为新的树根节点
}

//右旋函数
void RightRotate(AVLTree *T)
{//与左旋函数操作相反,不再赘述
	AVLTree L;
	L = (*T)->lchild;
	(*T)->lchild = L->rchild;
	L->rchild = (*T);
	(*T) = L;
}


//AVL树删除关键字操作,*lower用来记录插入节点后树是否有变矮
int DeleteAVL(AVLTree *T, int key, int *lower)
{
	if (!(*T))
	{//若当前树为空树,则删除失败返回0
		return 0;
	}
	else
	{
		if (key == (*T)->data)
		{						//若当前节点等于欲删除关键字
			Delete(T);			//则调用删除函数
			(*lower) = 1;		//树变矮
			return 1;
		}
		else if (key < (*T)->data)
		{//若欲删除关键字小于当前节点值
			if (!DeleteAVL(&(*T)->lchild, key, lower))
			{//如果左子树也找不到等于此关键字的节点值,删除失败
				return 0;
			}
			
			if (*lower)
			{//若删除成功,则*lower必然等于1,此时只要根据当前节点的平衡因子来调整树达到平衡
				switch ((*T)->bf)
				{								//删除的节点在当前节点的左子树的某处
					case LH:					//如果原来左子树高度就比右子树高1
						(*T)->bf = EH;			//则删除后,两边等高,故(*T)->bf = EH
						(*lower) = 1;			//相比原树,树变矮
						break;
					case EH:					//如果原来左子树与右子树等高
						(*T)->bf = RH;			//则删除后右子树比较高
						(*lower) = 0;			//左边树变矮,右边不变,但总体树高没变矮
						break;
					case RH:					//如果原来右子树高度就比左子树高1
						RightBalance(T);		//则删除后,右子树比左子树高2,故需要调用右平衡函数
						(*lower) = 1;			//调整过后,相比原树,树变矮
						break;
				}
			}
		}
		else
		{//若欲删除关键字大于当前节点值,道理同上,不再赘述
			if (!DeleteAVL(&(*T)->rchild, key, lower))
			{
				return 0;
			}
			
			if (*lower)
			{
				switch ((*T)->bf)
				{
					case LH:
						LeftBalance(T);
						(*lower) = 1;
						break;
					case EH:
						(*T)->bf = LH;
						(*lower) = 0;
						break;
					case RH:
						(*T)->bf = EH;
						(*lower) = 1;
						break;
				}
			}
		}
		return 1;
	}
}


//AVL树删除节点以及后续结构调整函数
int Delete(AVLTree *p)
{//*p为当前要删除的树根节点,为了满足二叉搜索树的性质,分情况讨论
	AVLTree q, s;//定义两个节点指针,q是s的父节点
	
	if ((*p)->rchild == NULL)
	{							//如果当前欲删除节点没有右子树(孩子)
		q = (*p);				//则直接将当前节点的左孩子作为新的树根节点
		(*p) = (*p)->lchild;	
		free(q);				//释放节点占用内存
	}
	else if ((*p)->lchild == NULL)
	{							//道理同上
		q = (*p);
		(*p) = (*p)->rchild;
		free(q);
	}
	else
	{//若当前欲删除节点既有左子树又有右子树,则取此树中序遍历最接近欲删除节点的那个前驱(后继)节点作为新的树根节点
		q = (*p);				//将当前欲删除节点赋给q
		s = (*p)->lchild;		//s是当前欲删除节点的左孩子,
		
		while (s->rchild)
		{						//此处循环为了寻找欲删除节点左孩子的最右孩子,即欲删除节点的中序遍历前驱节点
			q = s;				//如果s有右孩子,则将s赋给q,s则替换为其右孩子,即q是s的父节点
			s = s->rchild;
		}
		
		(*p)->data = s->data;	//经过上述操作,s就是欲删除节点的中序遍历前驱节点,我们不是直接删除*p节点,而是
								//将s的值赋给欲删除节点*p,然后删除s节点就可以达到调整的目的
		if (q != (*p))
		{								//若q不等于*p,说明q在while循环中被改变了,即s被替换为了其最右孩子
			q->rchild = s->lchild;		//删除s节点后,不保证s不含左孩子,故将其s的左孩子赋给父节点q的右孩子
		}
		else
		{								//若q等于*p,说明while循环体里的代码没有执行,即s不含右孩子,此时
			q->lchild = s->lchild;		//删除s节点后,应该把s的左孩子赋给q的左孩子,不然就缺胳膊短腿了
		}
		
		free(s);				//由于*p的值已被替换,结构也已调整完成,释放s节点占用内存
	}
	
	return 1;
}


//关键字查找函数,与二叉搜索树的查询函数一致,不再赘述,
//若有疑问请参考二叉搜索树(BST) ---- C语言
int SearchAVL(AVLTree T, int key, AVLTree f, AVLTree *p)
{
	if (!T)
	{
		(*p) = f;
		return 0;
	}
	else
	{
		if (key == T->data)
		{
			(*p) = T;
			return 1;
		}
		else if (key < T->data)
		{
			return SearchAVL(T->lchild, key, T, p);
		}
		else
		{
			return SearchAVL(T->rchild, key, T, p);
		}
	}
}

//树的中序遍历函数(递归)
void InOrderView(AVLTree T)
{
	if (!T)
	{
		return;
	}
	
	InOrderView(T->lchild);
	printf("%d--", T->data);
	InOrderView(T->rchild);
}


int main()
{
	int i, taller, lower;
	int a[10] = {33, 4, 12, 58, 99,
				40, 67, 55, 1, 120};
	AVLTree p, T = NULL;
	
	for (i = 0; i < 10; i++)
	{
		InsertAVL(&T, a[i], &taller);
	}
	
	InOrderView(T);
	
	SearchAVL(T, 55, NULL, &p);
	printf("\nSearch--p: %d", p->data);
	
	DeleteAVL(&T, 55, &lower);
	
	puts("\n");
	InOrderView(T);
	
	SearchAVL(T, 55, NULL, &p);
	printf("\nSearch--p: %d", p->data);
	
	return 0;
}

运行结果:

《平衡二叉树(AVL树) ----- C语言》

AVL树的实现代码比较复杂,主要要理解平衡的概念,左平衡跟右平衡各有两种情况,总共四种情况,而左旋要看其右孩子是否含有左孩子,右旋也同理,总之,多看代码,多画辅助图,容易加深理解,能力有限,谢谢阅读!

    原文作者:平衡二叉树
    原文地址: https://blog.csdn.net/u012538536/article/details/50354604
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞