AVL树的插入与删除操作

坚强是说给别人听的谎言 提交于 2019-11-29 21:09:43

AVL树是一种较老的数据结构,它的出现是为了解决了二叉查找树在最坏情况下的插入结果。

二叉查找树(以下称为二叉树)如果能以较好的插入序列来创建,使得树的结构趋于平衡,则其大部分操作都可以O(logN)的复杂度实现。但如果以类似{1,2,3,4,5,6,.....}这种已序序列来进行插入,那么形成的二叉树将是极不平衡的,甚至有可能只有左子树或只有右子树,这样的二叉树与链表是没有太大区别的,各种操作的复杂度也将升至O(N)这种无法接受的线性复杂度。

如果可以为二叉树设置一种限定条件,使得对于每个根节点,其左子树与右子树的高度差在一个可以接受的范围内(实现中为1),那么二叉树的平衡性就可以保证,以上就是AVL树的原始的设计理念。

由此引发了两个问题:1.我们怎样才能够随时获取每个节点的高度来作为是否平衡的信息呢?

                                        2. 我们如何调整树的结构(实际上是对指针的调整)来使得树从不平衡变为平衡?

问题1有两种解决方案,一种是设计一个函数,其接受一个节点参数,返回此节点的高度。这种函数式的思想是自然而然的,由于此函数可以使用递归实现,其编程复杂度也不高。但是,如果树的高度足够高,在同一时间,我们为了一个简单信息,有可能在在同一时间内,造成大量函数栈在内存中堆叠。越是庞大的二叉树,这种方式的成本就越高,有点得不偿失。

另一种解决方案是,为每一个节点,保存其高度信息,并在插入、删除操作之后更新这些节点信息。因为插入、删除操作皆可以递归实现,更新这些信息是非常简单的。此方案虽增加了O(N)的空间占用,但是使得编程复杂度降低,并且避免了栈的堆叠。所以较第一种方案,有一定的优势。


问题2的解决方法是AVL树实现的核心所在。我们可以以一种微妙的操作来使树的高度差减小,使得二叉树趋于平衡。


第一种方法名字叫单旋转,其分为两种情况:

1.(此时X造成了不平衡)

2.(此时Z造成了不平衡)

以第一个情况为例:X代表了两层的节点,Y、Z为一层,k1、k2为两个根节点。在数据结构与算法分析(c语言描述)这本书种,作者很形象的将这种旋转方式比喻成“提起”。

比如,我们可以将第一个图的右边(旋转完成后)想象成用手捏着k1节点向上提起,k2在重力作用下变成了k1的右子树,而与此同时,Y也吸附似的变成了k2的左儿子(可以把Y想象成一个金属,而k2是一个磁铁)。

重点在于我们何时应该使用这种旋转方式呢?先看一下第二种名为双旋转的平衡方式。(同样也有两种情况,在这只给出第一种)


(注意,此时k2与B、C组成的大节点造成了不平衡)我们在理解这种平衡方式时可以看作实行了两次单旋转:一次是k1与k2之间的,第二次是k3与k1、k2单旋转的结果之间的。(实际上,双旋转的编程实现就是如此)


现在我们可以总结一下规律:不知大家发现没有,需要单旋转的不平衡现象是由整个子树左右最外侧的两个路径上的节点引起的,而双旋转是由较内侧的那个路径上的节点引起的(我们将k2和B、C看作一个大节点,A节点就属于外侧路径上的节点,旋转以后,D节点也属于外侧路径上的节点),这个规律是在数据结构与算法分析(c语言描述)书中P81中描述的通俗理解,在下面的AVL树的删除操作中就使用了这种方法。


在插入操作中,对于单旋转双旋转的选择还有一个规律:如果插入的节点造成了不平衡,那么当   插入节点的键值<其父节点的键值<其祖父节点的键值  或插入节点的键值>其父节点的键值>其祖父节点的键值   那么实行单旋转 


当   其父节点的键值<插入节点的键值<其祖父节点的键值 或  其父节点的键值>插入节点的键值>其祖父节点的键值 实行单旋转


好了,基本情况就这些,代码在下边,一定要亲手实践一下才可以哟~

struct avlnode;
typedef avlnode * AvlTree;
typedef avlnode * AvlPosition;
typedef int ElementType;
struct avlnode
{
	ElementType Element;
	int Height;
	avlnode * Left;
	avlnode * Right;
};
static int Max(int n1, int n2)
{
	return n1 > n2 ? n1 : n2;
}
static int Height(AvlPosition P)
{
	if (P == NULL)
		return -1;
	else
		return P->Height;
}
static AvlPosition SingleWithRotateLeft(AvlPosition K2)
{//单旋转第一种情况
	AvlPosition K1 = K2->Left;
	K2->Left = K1->Right;
	K1->Right = K2;
    K1->Height = Max(Height(K1->Left), Height(K1->Right))+1;
	K2->Height = Max(Height(K2->Left), Height(K2->Right))+1;
	return K1;
}
static AvlPosition SingleWithRotateRight(AvlPosition K1)
{//单旋转第二种情况
	AvlPosition K2 = K1->Right;
	K1->Right = K2->Left;
	K2->Left = K1;
	K1->Height = Max(Height(K1->Left), Height(K1->Right))+1;
	K2->Height = Max(Height(K2->Left), Height(K2->Right))+1;
	return K2;
}
static AvlPosition DoubleWithRotateLeft(AvlPosition K3)
{//双旋转第一种情况
	K3->Left = SingleWithRotateRight(K3->Left);
	return SingleWithRotateLeft(K3);
}
static AvlPosition DoubleWithRotateRight(AvlPosition K3)
{//双旋转第二种情况
	K3->Right = SingleWithRotateLeft(K3->Right);
	return SingleWithRotateRight(K3);
}
AvlPosition AvlInsert(ElementType X, AvlTree T)
{
	if (T == NULL)
	{//找到合适位置,准备插入
		T = (AvlPosition)malloc(sizeof(avlnode));
		if (T == NULL)
			exit(1);
		T->Left = T->Right = NULL;
		T->Height = 0;
		T->Element = X;
	}
	else if (X < T->Element)
	{//X应插入到T的左子树中
		T->Left = AvlInsert(X, T->Left);
		if (Height(T->Left) - Height(T->Right) == 2)
			//由于在左子树的插入导致在T处平衡被破坏
			if (X < T->Left->Element)
				//如果新插入的元素的值X满足:T->Left<X<T 则执行单旋转
				T=SingleWithRotateLeft(T);
			else
				//否则执行双旋转
				T=DoubleWithRotateLeft(T);
	}
	else if (X > T->Element)
	{//X应插入到T的右子树中
		T->Right = AvlInsert(X, T->Right);
		if (Height(T->Right) - Height(T->Left) == 2)
			//由于在右子树的插入导致在T处平衡被破坏
			if (X>T->Right->Element)
				//如果新插入的元素的值X满足:T<X<T->Right 则执行单旋转
				T=SingleWithRotateRight(T);
			else
				//否则执行双旋转
				T=DoubleWithRotateRight(T);
	}
	T->Height = Max(Height(T->Left), Height(T->Right)) + 1;
	return T;
}

AVL的删除操作实现起来比较困难,有一种方法是再在节点中添加一个元素,用来指示其左右子树是否平衡,但这样的话乱糟糟的,尤其是在网上看到很多还使用了switch结构,真的是一锅粥了。。。 下面这个方法应该是比较简单的~

AvlPosition AvlDelete(ElementType X, AvlTree T)
{
	AvlPosition Temp;
	if (T == NULL)
		return NULL;
	else if (X < T->Element)
	{
		T->Left=AvlDelete(X, T->Left);
		if (Height(T->Right) - Height(T->Left) == 2)
			//由于在左子树的删除导致在T处平衡被破坏
			if (Height(T->Right->Left)>Height(T->Right->Right))
				//如果T->Right的左子树比右子树高,执行双旋转
				DoubleWithRotateLeft(T);
			else
				//否则执行单旋转
				SingleWithRotateLeft(T);
	}
	else if (X > T->Element)
	{
		T->Right=AvlDelete(X, T->Right);
		if (Height(T->Left) - Height(T->Right) == 2)
			//由于在右子树的删除导致在T处平衡被破坏
			if (Height(T->Left->Left)>Height(T->Left->Right))
				//如果T->Left的左子树比右子树要高,执行单旋转
				SingleWithRotateRight(T);
			else
				//否则执行双旋转
				DoubleWithRotateRight(T);
	}
	else if (T->Left&&T->Right)
	{//正常的删除策略
		Temp = AvlFindMin(T->Right);
		T->Element = Temp->Element;
		T->Right = AvlDelete(T->Element, T->Right);
	}
	else
	{
		Temp = T;
		if (T->Left == NULL)
			T = T->Right;
		else if (T->Right == NULL)
			T = T->Left;
		free(Temp);
	}
	if (T!=NULL)
	//最后更新高度,此处可能T已经被设置成了NULL,所以需要检测下
	T->Height = Max(Height(T->Left), Height(T->Right)) + 1;
	return T;
}


基本上就这些~

AVL树这种平衡策略比较老了,而且编程也比较复杂,现在用处不多,不过理解了AVL树可以帮助我们更好的理解RB树,伸展树,B-树等其他的平衡策略

最后,复杂度都是O(logN)是没问题了(AVL树的目的就是这个~)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!