平衡二叉树(AVL)的插入和删除详解(上)

AVL树维基百科:http://zh.wikipedia.org/wiki/AVL树

在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G.M. Adelson-Velsky和E.M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

原理请看上面维基百科词条,可以参考严蔚敏数据结构或其它书籍,这里就不对原理做过多解释了,下面将直接给出其实现,代码有详细注释。

1、基本约定

使用平衡二叉树就是为了高效的查找,一般是根据关键字查找记录,而记录一般是复杂的类型对象。这里我们以一个Student类作为记录类型,学号作为关键字。

我们假定所使用的元素类型,都能进行各种比较和赋值。用LH,EH,RH分别表示左子树高,等高,右子树高,即平衡因子-1、0、1。

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

#define EQ(a,b) ((a) == (b))
#define LT(a,b) ((a) < (b))
#define LQ(a,b) ((a) <= (b))

//结点元素类型 
typedef struct Student
{
	int key;
	string major;
	Student(){}
	Student(int k,string s) : key(k), major(s){}
}ElementType;

ostream& operator<<(ostream& out, const Student& s)
{
	out<<"("<<s.key<<","<<s.major<<")";
	return out;
}

istream& operator>>(istream& in,Student& s)
{
	in>>s.key>>s.major;
}

typedef int KeyType;//关键字类型 

typedef struct AVLNode
{
	ElementType data;
	int bf;
	struct AVLNode* lchild;
	struct AVLNode* rchild;
	
	AVLNode(){}
	AVLNode(ElementType& e, int ibf=EH, AVLNode* lc=NULL, AVLNode* rc=NULL)
		: data(e), bf(ibf), lchild(lc),rchild(rc){}
}AVLNode, *AVL;

2、初始化、销毁

/*
  *Description: 初始化(其实可以不用)
*/
void initAVL(AVL& t)
{
	t = NULL;
}

/*
  *Description: 销毁平衡二叉树 
*/
void destroyAVL(AVL& t)
{
	if(t)
	{
		destroyAVL(t->lchild);
		destroyAVL(t->rchild);
		delete t;
		t = NULL;
	}
}

3、遍历和查找

//前序遍历
void preOrderTraverse(AVL t)
{
	if(t)
	{
		cout<<t->data<<" ";
		preOrderTraverse(t->lchild);
		preOrderTraverse(t->rchild);
	}
} 

//中序遍历
void inOrderTraverse(AVL t)
{
	if(t)
	{
		inOrderTraverse(t->lchild);
		cout<<t->data<<" ";
		inOrderTraverse(t->rchild);
	}
} 

//以前序和中序输出平衡二叉树
void printAVL(AVL t)
{
	cout<<"inOrder: "<<endl;
	inOrderTraverse(t);
	cout<<endl;
	cout<<"preOrder: "<<endl;
	preOrderTraverse(t);
	cout<<endl;
}
 
/*
  Description: 
		在根指针t所指平衡二叉树中递归地查找某关键字等于key的数据元素, 
		若查找成功,则返回指向该数据元素结点的指针,否则返回空指针。
		根据需要,也可以返回一个bool值 
*/
AVLNode* searchAVL(AVL& t, KeyType key)
{
	if((t == NULL)||EQ(key,t->data.key))
		return t; 
   	else if LT(key,t->data.key) /* 在左子树中继续查找 */
     	return searchAVL(t->lchild,key);
   	else
     	return searchAVL(t->rchild,key); /* 在右子树中继续查找 */
}

4、旋转处理

左旋和右旋,大家记住“左逆右顺”就可以了。

(1)左旋-逆时针旋转(如RR型就得对根结点做该旋转)

/*
  Description: 
		对以*p为根的二叉排序树作左旋处理,处理之后p指向新的树根结点,即旋转 
		处理之前的右子树的根结点。也就是书上说说的RR型. 
*/
void L_Rotate(AVLNode* &p)
{
	AVLNode * rc = NULL;
	rc = p->rchild;			//rc指向p的右子树根结点
	p->rchild = rc->lchild;//rc的左子树挂接为p的右子树 
	rc->lchild = p;
	p = rc;					//p指向新的根结点 
}

(2)右旋-顺时针旋转(如LL型就得对根结点做该旋转)

/*
  Description:
		对以*p为根的二叉排序树作右旋处理,处理之后p指向新的树根结点,即旋转 
		处理之前的左子树的根结点。也就是书上说说的LL型. 
*/
void R_Rotate(AVLNode* &p)
{ 
	AVLNode * lc = NULL;
	lc  = p->lchild;		//lc指向p的左子树根结点
	p->lchild = lc->rchild;	//lc的右子树挂接为p的左子树 
	lc->rchild = p;
	p = lc;					//p指向新的根结点 
}

5、左平衡处理

所谓左平衡处理,就是某一根结点的左子树比右子树过高,从而失去了平衡。

(1)插入时如果需要左平衡处理,根结点左子树根平衡因子只可能为LH和RH。

(2)删除和插入不同,根结点左子树根的平衡因子三种情况都可能出现,因为是删除根结点右子树中的结点从而引起左子树过高,在删除前,根结点左子树根的平衡因子是可以为EH的,此种情况同样是对根结点做简单右旋处理。

/*对以指针t所指结点为根的二叉树作左平衡旋转处理
	包含LL旋转和LR旋转两种情况 
	平衡因子的改变其实很简单,自己画图就出来了 
*/
void leftBalance(AVLNode* &t)
{
	AVLNode* lc = NULL;
	AVLNode* rd = NULL;
	lc = t->lchild;
	switch(lc->bf)
	{
		case LH:					//LL旋转 
			t->bf = EH;
			lc->bf = EH;
			R_Rotate(t);		
			break;
		
		case EH:					//deleteAVL需要,insertAVL用不着 
			t->bf = LH;
			lc->bf = RH;
			R_Rotate(t);
			break;
		
		case RH:					//LR旋转 
			rd = lc->rchild;
			switch(rd->bf)
			{
				case LH:
					t->bf = RH;
					lc->bf = EH;
					break;	
				case EH:
					t->bf = EH;
					lc->bf = EH;
					break;
				case RH:
					t->bf = EH;
					lc->bf = LH;
					break;
			}
			rd->bf = EH;
			L_Rotate(t->lchild);//不能写L_Rotate(lc);采用的是引用参数 
			R_Rotate(t);
			break;
	}
}

6、右平衡处理

类似左平衡处理,所谓右平衡处理,就是某一根结点的右子树比左子树过高,从而失去了平衡。

(1)插入时如果需要右平衡处理,根结点右子树根平衡因子只可能为LH和RH。

(2)删除和插入不同,根结点右子树根的平衡因子三种情况都可能出现,因为是删除根结点左子树中的结点从而引起右子树过高,在删除前,根结点右子树根的平衡因子是可以为EH的,此种情况同样是对根结点做简单左旋处理。

/*对以指针t所指结点为根的二叉树作右平衡旋转处理
	包含RR旋转和RL旋转两种情况 
*/
void rightBalance(AVLNode* &t)
{
	AVLNode* rc = NULL;
	AVLNode *ld = NULL;
	
	rc = t->rchild;
	switch(rc->bf)
	{
		case LH:				//RL旋转 
			ld = rc->lchild; 
			switch(ld->bf)
			{
				case LH:
					t->bf = EH;
					rc->bf = RH;
					break;
				case EH:
					t->bf = EH;
					rc->bf = EH;
					break;
				case RH:
					t->bf = LH;
					rc->bf = EH;
					break;
			}
			ld->bf = EH;
			R_Rotate(t->rchild);//不能写R_Rotate(rc);采用的是引用参数 
			L_Rotate(t);
			break;
			
		case EH:				//deleteAVL需要,insertAVL用不着 
			t->bf = RH;
			rc->bf = LH;
			L_Rotate(t);
			break;
				
		case RH:				//RR旋转 
			t->bf = EH;
			rc->bf = EH;
			L_Rotate(t);
			break;
	}
}

7、插入处理

在插入一个元素时,总是插入在一个叶子结点上。我们采用递归插入,也就是不断搜索平衡二叉树,找到一个合适的插入点(当然相同关键字不插入)。插入后,引起的第一个不平衡的子树的根结点,一定是在查找路径上离该插入点最近的,注意看代码中递归后的回溯。

/* 
若在平衡的二叉排序树t中不存在和e有相同关键字的结点,则插入一个 
数据元素为e的新结点,并返回true,否则返回false。若因插入而使二叉排序树 
失去平衡,则作平衡旋转处理,布尔变量taller反映t长高与否
*/
bool insertAVL(AVL& t, ElementType& e, bool& taller)
{
	if(t == NULL)
	{
		t = new AVLNode(e);				//插入元素 
		taller = true;
	}
	else
	{
		if(EQ(e.key, t->data.key))		//树中已含该关键字,不插入 
		{
			taller = false;
			return false;
		}
		else if(LT(e.key, t->data.key))//在左子树中查找插入点 
		{
			if(!insertAVL(t->lchild, e, taller))//左子树插入失败 
			{
				return false;
			}
			if(taller)					//左子树插入成功,且左子树增高 
			{
				switch(t->bf)
				{
					case LH:			//原来t的左子树高于右子树 
						leftBalance(t); //做左平衡处理 
						taller = false;
						break;
					case EH:			//原来t的左子树和右子树等高 
						t->bf = LH;		//现在左子树比右子树高 
						taller = true;	//整棵树增高了 
						break;
					case RH:			//原来t的右子树高于左子树
						t->bf = EH;		//现在左右子树等高 
						taller = false;
						break;
				}
			}
		}
		else							//在右子树中查找插入点 
		{
			if(!insertAVL(t->rchild, e, taller))//右子树插入失败 
			{
				return false;
			}
			if(taller)					//右子树插入成功,且右子树增高
			{
				switch(t->bf)
				{
					case LH:			//原来t的左子树高于右子树 
						t->bf = EH;
						taller = false;
						break;
					case EH:			//原来t的左子树和右子树等高 
						t->bf = RH;
						taller = true;
						break;
					case RH:			//原来t的右子树高于左子树
						rightBalance(t);//做右平衡处理
						taller = false;
						break;
				}
			}
		}
	}
	return true;						//插入成功 
}

8、删除处理

删除和插入不同的是,删除的结点不一定是叶子结点,可能是树中的任何一个结点。前面在讲解二叉查找树时,我们知道删除的结点可能有三种情况:(1)为叶子结点,(2)左子树或右子树有一个为空,(3)左右子树都不空。对第三种情况的处理我们介绍了三种处理方式,这里我们采用删除前驱的方式。注意到我们仍然采用的是递归删除,然后判断删除后树是否“变矮”了,然后进行相应的处理。对(1)(2)中情况,很好处理,树的确是“变矮”了。对于第(3)种情况,我们不能直接找到前驱结点,然后把数据拷贝到原本要删除的根结点,最后直接删除前驱结点。因为这么做,我们无法判断原先根结点子树高度的变化情况。所以我们在找到前驱结点后,不是直接删除,而是采用在根结点左子树中递归删除前驱的方式。

/* 
若在平衡的二叉排序树t中存在和e有相同关键字的结点,则删除之 
并返回true,否则返回false。若因删除而使二叉排序树 
失去平衡,则作平衡旋转处理,布尔变量shorter反映t变矮与否
*/
bool deleteAVL(AVL& t, KeyType key, bool& shorter)
{
	if(t == NULL)						//不存在该元素 
	{
		return false;					//删除失败 
	}
	else if(EQ(key, t->data.key))		//找到元素结点
	{
		AVLNode* q = NULL;
		if(t->lchild == NULL)			//左子树为空 
		{
			q = t;
			t = t->rchild;
			delete q;
			shorter = true;
		}
		else if(t->rchild == NULL)		//右子树为空 
		{
			q = t;
			t = t->lchild;
			delete q;
			shorter = true;
		}
		else							//左右子树都存在,
		{
			q = t->lchild;
			while(q->rchild)
			{
				q = q->rchild;
			}
			t->data = q->data;
			deleteAVL(t->lchild, q->data.key, shorter);	//在左子树中递归删除前驱结点 
		}
	}
	else if(LT(key, t->data.key))		//左子树中继续查找 
	{
		if(!deleteAVL(t->lchild, key, shorter))
		{
			return false;
		}
		if(shorter)
		{
			switch(t->bf)
			{
				case LH:
					t->bf = EH;
					shorter = true;
					break;
				case EH:
					t->bf = RH;
					shorter = false;
					break;
				case RH:
					rightBalance(t);	//右平衡处理
					if(t->rchild->bf == EH)//注意这里,画图思考一下 
						shorter = false;
					else
						shorter = true;
					break;
			}
		}
	}
	else								//右子树中继续查找 
	{
		if(!deleteAVL(t->rchild, key, shorter))
		{
			return false;
		}
		if(shorter)
		{
			switch(t->bf)
			{
				case LH:
					leftBalance(t);		//左平衡处理 
					if(t->lchild->bf == EH)//注意这里,画图思考一下 
						shorter = false;
					else
						shorter = true;
					break;
				case EH:
					t->bf = LH;
					shorter = false;
					break;
				case RH:
					t->bf = EH;
					shorter = true;
					break;
			}
		}
	}
	return true;
}

注:

(1)在平衡二叉树(AVL)插入和删除详解(下)中给出测试代码和测试用例,并给出了一个涵盖所有情况的图例。

平衡二叉树的插入和删除详解(下): http://blog.csdn.net/sysu_arui/article/details/7906303

(2)也许大家难以理解的是插入和删除过程中,平衡因子的变化。其实,插入和删除都是递归进行的,平衡因子的变化是在递归回溯过程中,自底向上改变的,至于怎么改变,把几种情况弄清楚之后,画图就出来了。

(3)另一种简单的C++实现,不用计算平衡因子:http://blog.csdn.net/sysu_arui/article/details/7921498

(4)建议看看二叉查找树的插入和删除,对比分析一下:http://blog.csdn.net/sysu_arui/article/details/7865864

参考资料:

[1]严蔚敏 数据结构(C语言版)

[2]一篇博客文章:http://blog.sina.com.cn/s/blog_66aeefeb010127ht.html (注意文章中,插入操作有点错误)

[3]另一种C实现:http://ishare.iask.sina.com.cn/f/25570459.html?retcode=0

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