c++二叉树_C/C++后台开发学习笔记(六)-平衡二叉树

026be8b9739fdc9c3b9a67b00097b46c.png

一、平衡二叉树

(1)平衡二叉树介绍

平衡二叉树是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1.
平衡二叉树也称为AVL树。
平衡二叉树是一种高度平衡的二叉排序树,要么它是一颗空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度差绝对值不超过1。我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),那么平衡因为可以取值为-1,0,1,如果二叉树上有一个结点的平衡因子的绝对值大于1,就说明该二叉树不平衡.距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树

107ed87be995da47cdd69766031776ce.png


56和58和60结点就构成一个最小的不平衡树。

(2)实现原理

平衡二叉树的实现原理也是比较简单的,就是在插入的时候,检查插入的结点是否破坏平衡二叉树的平衡性,如果是,就找出最小不平衡树,在保存二叉树的前提下,进行旋转操作,让旋转后的树满足平衡二叉树。
还是举我们上节课的例子:
原始数据:12, 45, 89, 127, 7, 4, 56, 60, 9,14,16,58
最后我们二叉排序树的结果:

61172a5840cde700f4bce11567adbb9a.png


这个图明显是左轻右重,树的层数竟然是5层,查找的效应降低了很多,我举个这里例子不极端,极端的是斜树的情况会退化到O(n),但是也能作为学习平衡二叉树的例子了。当我们学会了平衡二叉树,构建的树就会平衡很多了,下面详细说说这个平衡二叉树创建的过程:

  1. 插入结点12

400b1cd72a8044c38a9bf33fbdadc73f.png
  1. 接着插入结点45

68bf8df73ca0adad1dd9f847bae046b2.png
  1. 插入个89,现在就要看bf的值了

69653184428473c29fe1a84ab70930ad.png


这个时候结点12的BF=-2,左子树为0,右子树为2,所以BF=-2,-2已经打破平衡性了,所以需要旋转,看着上图明显是左轻右重,因为往左边旋转,旋转的结果就是结点12变成45的左孩子,45变成了根结点,如图:

9642750ccb6a9dba3ec8460fb6bab504.png


旋转过后,是不是很符合平衡二叉树的性质。

  1. 接下来插入127,然后插入一个7

74f27e5bdd265c0539fbed96a363eb61.png
  1. 插入4,插入4的时候,平衡又被打破了,再次需要旋转

a4a8e0f56459fe72e42c05cefa77b0bf.png


结点12的BF又再次大于2,这次是左重右轻,需要右旋,右旋就是把12的结点变成结点7的右孩子,结点7替代结点12的位置,如图:

b1ae942614f9fc9105593656e17edbf0.png


旋转完成之后,又变回原来的平衡状态。

  1. 插入56

6909691cd1d7a8a9ded7a0de1afdc535.png


插入56的时候很平衡

  1. 插入60

e58f7e5c0802817f3c05c51421d0fa67.png
  1. 插入9,14,16之后平衡二叉树再次不平衡

fd3a530aa39fbafd2db87eead845ce0b.png


这个是结点7的BF为-2,左轻右重,需要左旋,左旋的结果就是把12的左子树的挂到结点7的右子树上,然后12替代7的位置,7变成12的左子树,如图:

7d2cf3953542d4cd47d2f4f8b05b1ea6.png

平衡二叉树再次平衡

  1. 插入最后一个结点58

39d0e2d3b93e62172c7bc034845a9c26.png


这次平衡再次被打破,需要旋转,如图56结点的BF=-2,左轻右重,理论上是需要左旋,但是左旋之后,结点58变成了56的左子树,60替换56的位置,如图:

34cf019bc9969b53a233626773384179.png


效果并没有改善,这里为什么会这样,是因为60结点的BF是正的,而56结点的BF是负的,所以这种情况不能简单的旋转,需要双旋,60和58结点先有旋,

432fdabd0e8d1e91ada12e3cbc251413.png


这样58和56结点的符号就是同一方向了,现在可以左旋操作了,左旋操作之后

6b609e0732d63579edef93fab845f8eb.png


这就是一颗平衡二叉树,并且插入完成。

(3)左旋

平衡二叉树的基本操作是旋转,我们先介绍左旋,左旋的操作在上面插入的时候应该说过了,这里再详细画一个图介绍介绍:

213d4df3974a9f2321f96f8f99be7668.png


图中的虚线圈起来是一个最小不平衡树,这个树左轻右重,需要左旋,左旋的规则:左旋时候主需要关注最小不平衡树,12结点就是这个树的根结点,14结点就是根结点的右子树为:R,13是Rl,16是Rr,下来用图来表示:

897f7d6e6d2ebe2cf74fc5b185960437.png


Rl结点挂在根结点的右子树上,R结点替换根结点,根结点成为R的左子树,Rr结点不动。左旋之后的结果:

b489bdef87e40aea04f1bdb80fa064a8.png


左旋之后,就形成了平衡二叉树,这就是左旋结果。
突然发现讲了这么久都没讲平衡二叉树的数据结构:

typedef int Elemtype;
	
#define BITREE_ENTRY(name, type)	
		struct name 					
		{								
			struct type *left;			
			struct type *right; 		
			int    bf;									//结点的平衡因子
		}
	
typedef struct AVLTree_node
{
	Elemtype data;								//结点数据
	BITREE_ENTRY(, BiTree_node) bst;			//左右孩子的结点
}_AVLTree_node;

typedef struct AVLTree
{
	struct AVLTree_node *root;
}_AVLTree;

看着这个数据跟二叉树的差不多,只不多是每个结点多了一个表示平衡因子的,但是目前我们只关心左旋,所以只要左旋就好,下面是左旋代码:

/**
		* @brief  AVL树左旋
		* @param  
		* @retval 
		*/ 
		static int AVLTree_L_Rotate(struct AVLTree_node **node)
		{
			//node为这颗最小不平衡树的根结点(node结点就是他的父结点的左右指针的一个)
			struct AVLTree_node *root = *node;

			//获取根结点的右子树 R
			struct AVLTree_node *R_node = root->bst.right;

			//把Rl挂接到根结点的右子树上
			root->bst.right = R_node->bst.left;
			
			//根结点成为R的左子树
			R_node->bst.left = root;

			//R替换根结点
			*node = R_node;    //把父结点的孩子指针指向了R结点

			return 0;
		}

看懂了上面的左旋原理,这个程序也就不难了,难点就是传入的node的二重指针是为什么?其实这是为了修改这个指针的指向,修改这个指针的指向,也就是修改了node指针的父节点的左右孩子中其中的一个的指针,也就是把node的父结点的孩子结点换了。

(4)右旋

上面讲解了左旋,右旋是跟左旋是对称的关系,可以看下需要右旋时候的最小平衡树:

2a2b640b43e5fdb6a5b7ce08af98078a.png


14结点BF=2,是这个最小不平衡树的根结点,并且BF=2,表示左重右轻,需要右旋,相关结点描述,12结点是L,9结点是Ll,13结点是Lr结点,右旋步骤:Lr结点挂载在根结点上,12结点替换根结点,原来的根结点变为12结点的右子树。其实跟左旋是对称的。

2c12cb8be511260da0c6d53d27ad3acd.png


进行右旋操作:

8ee633e2344982355fa75bdfe2146501.png


右旋之后,最小不平衡树又平衡了,这就是算法的魅力,下面看看代码:

/**
		* @brief  AVL树右旋
		* @param  
		* @retval 
		*/ 
		static int AVLTree_R_Rotate(struct AVLTree_node **node)
		{
			//node为这颗最小不平衡树的根结点(node结点就是他的父结点的左右指针的一个)
			struct AVLTree_node *root = *node;

			//获取根结点的左子树 L
			struct AVLTree_node *L_node = root->bst.left;

			//把Lr挂接到根结点的左子树上
			root->bst.left = L_node->bst.right;

			//根结点成为L的右子树
			L_node->bst.right = root;
			
			//L替换根结点
			*node = L_node;    //把父结点的孩子指针指向了L结点

			return 0;
		}

看了代码跟左旋代码是差不多的。

(5)平衡二叉树插入

我觉得还是先讲插入比较好,因为左右平衡已经关系到整颗树了,没有整颗树的概念,就直接讲左右平衡可能理解不了,平衡二叉树比二叉排序树多了一个bf平衡因子这个变量,所以平衡二叉树每次插入的时候都需要调整这个bf值,如果bf的绝对值大于2,需要旋转操作,这也造成了平衡二叉树插入的难度。
就平衡二叉树的插入操作,接下一波:

/**
		* @brief  AVL创建一个新结点
		* @param  
		* @retval 
		*/ 
		static struct AVLTree_node* AVLTree_creat_node(Elemtype data, int bf)
		{
			//申请一个结点
			struct AVLTree_node *node = (struct AVLTree_node *)malloc(sizeof(struct AVLTree_node));
			assert(node);

			//填充数据
			node->data = data;
			node->bst.bf = bf;
			node->bst.left = NULL;
			node->bst.right = NULL;

			return node;
		}
		
/**
		* @brief  AVL插入操作(用递归比较好)
		* @param  
		* @retval 
		*/ 
		static int AVLTree_Insert(struct AVLTree_node **node, Elemtype data, int *flag)
		{
			if(!(*node)) 			//递归调用,如果这个结点为空,说明插入的结点就是这个位置
			{
				*node = AVLTree_creat_node(data, EH);		//申请一个新的结点,并退出递归
				*flag = 1;
				printf("creat node %p %dn", *node, data);
				
			}
			else    				//如果这个结点存在,需要查找到插入的位置
			{
				if( data < (*node)->data )			//如果插入的data数据比结点的data小,需要从左子树寻找位置  
				{
					AVLTree_Insert(&((*node)->bst.left),  data, flag);		//这个需要左子树寻找位置,记得这里是递归

					//假如这里递归完成,需要调整bf值,有一下情况:
					
					printf("lflag = %d %dn", *flag, (*node)->bst.bf);

					if(*flag)
					{
						//递归调整平衡二叉树的bf,很巧妙
						switch((*node)->bst.bf)  //这个已经插入完成,开始回调修改bf值了
						{
							case EH:   //看图2,位置1
								(*node)->bst.bf = LH;
								*flag = 1;
								break;

							case LH:	
							//本来已经是左高了,现在插入左子树,左子树就超过了,需要左平衡处理,6.1.6会详细说明这种情况
								AVLTree_LeftBalance(node);
								*flag = 0;
								break;

							case RH:	//看图1  ,位置3
								(*node)->bst.bf = EH;
								*flag = 0;
								break;
						}
					}
				}
				else 
				{
					AVLTree_Insert(&((*node)->bst.right),  data, flag);     //这时候是往右子树插入

					//递归调整平衡二叉树的bf,很巧妙
					printf("Rflag = %d %dn", *flag, (*node)->bst.bf);
					if(*flag)
					{
						switch((*node)->bst.bf)  //这个已经插入完成,开始回调修改bf值了
						{
							case EH:   
								//右边跟左边是反方向的,这个相等的就不画了,根图2差不多,需要递归回去修改祖父结点的bf值
								(*node)->bst.bf = RH;
								*flag = 1;
								break;

							case LH:	
							//如果是左子树高,插入右子树就等高了,跟图1差不多,这个只需要修改父节点的bf值即可,不需要修改祖父结点的
								(*node)->bst.bf = EH;
								*flag = 0;
								break;

							case RH:	
							//本来已经是右高了,现在又插入右子树,右子树就超过了,需要右平衡处理,6.1.7会详细说明这种情况
								AVLTree_RightBalance(node);
								*flag = 0;
								break;
						}
					}
				}
			}
			
			return 0;
		}

图1:

e6c3300a36ba536295fa0491d1c39279.png


这个图1就是符合插入14的左孩子的时候,判断14结点的BF,BF=RH,所以符合位置3的情况,这种情况只要把14结点的BF=EH,其他结点不需要改动,就可以完成整个树的BF值的改动。

图2:

a4d9f30d25a4e5d376ba3fbdaf96f4a0.png


这个图2就是符合9结点插入左孩子的时候,9结点的BF=EH,这时候符合位置1的情况,这时候因为9结点添加了一个左孩子,所以BF=LH,因为这是树,所以需要反递归,把包含9结点的其他结点的BF都需要改变,所以往上反递归,12结点的BF=RH,所以符合图1的情况,所以这时候就把12结点的BF=EH了,这时候就稳定了,不需要再做修改了。

还有最后一个可能,就是这个结点已经是LH,左子树高,在插入左子树的话,就导致失去平衡了,所以这时候需要左平衡处理,这个左平衡处理要到6.1.6讲,这里就是简单的引入,整体把握平衡二叉树的插入。

(6)左平衡

从上一节平衡二叉树的插入,整体了解了插入的步骤,但是还是在做左平衡的时候没有说清楚,左平衡处理的时候也有几种情况需要处理,

  • 符号相同的情况

d1c75621f1d63d3fc5674699683b1b35.png


父结点没有子树的情况下,插入左孩子,导致祖父结点不平衡。

8ef078633c2052bd08c8080d13490b45.png


89结点是有左右子树的,这的时候在左子树上插入一个结点,会导致89结点的树失去平衡。

这两种情况是属于符号相同的时候的,因为是符号相同,所以只需要简单的右旋即可。

  • 符号相反的情况

7bf3e7c76498257e140a2724cac9e5cd.png


这种情况是最简单的,root结点和L_node结点的bf都=0

462d85ca5c79eebf76b23f125a6a9508.png


这个就比较难了,因为谁都有子树的情况下,这种情况我画了图在旋转了:

48dd2294939d047b773ec00eb4086228.png


这个就是上面的图左旋之后的结果,因为都挂着子树,想象力不够的话,还是画图吧,下一步是右旋:

4dd3e61139bf8141847ffc2706df1c54.png


右旋之后整个数就是平衡的了,从图中可以看出,以前的根结点的bf=0,以后的L_node的bf=LH了。

下面继续这种情况:

75e72954d1971d73a9f8dc7e5e488858.png


想象力不够,画图来解析,先左旋:

ce50c09a33d740742c93d2d701141fab.png


接下来右旋:

ffece4d628f3ac5ceb3bdc0ee91e5723.png


图画的是有点丑了,但是看树的高度也可以明白,root结点的BF=RH,L_node的bf=EH。下面是代码实现:

/**
		* @brief  AVL左平衡 (BF<0的时候的情况)
		* @param  
		* @retval 算法处理完之后,node指针已经被修改了值 
		*/ 
		int AVLTree_LeftBalance(struct AVLTree_node **node)
		{
			//node为这颗最小不平衡树的根结点(node结点就是他的父结点的左右指针的一个)
			struct AVLTree_node *root = *node;

			//获取根结点的左子树 L
			struct AVLTree_node *L_node = root->bst.left;
			struct AVLTree_node *Lr_node = L_node->bst.right;

			//根结点的BF<0的时候,   它的左子树的bf   有两种情况:
			printf("AVLTree_LeftBalance %dn", L_node->bst.bf);
			switch(L_node->bst.bf)
			{
				//新插入的元素是插在root的左子树上的时候,只需要右旋就好
				case LH:
					//右旋也是分两种情况,但是这两种情况的结果是一样的,根结点的bf值和L_node结点的bf值都为0
					root->bst.bf = L_node->bst.bf = EH;		
					AVLTree_R_Rotate(node);
					break;

				//如果是反向的话,就需要双旋了,双旋比较复杂有三种情况的
				case RH:
					switch(Lr_node->bst.bf)
					{
						case EH:
							//想等的情况最简单,记得这是处理完,双旋之后的结果,双旋之后的结果就是两个bf=0
							root->bst.bf = L_node->bst.bf = 0;
							break;

						//下面这两种情况,在上面的图解上已经分析过了,平衡二叉树的难点就是BF值的更新,真难
						case LH: 
							root->bst.bf = RH;
							L_node->bst.bf = EH;
							break;

						case RH:
							root->bst.bf = EH;
							L_node->bst.bf = LH;
							break;
					}

					Lr_node->bst.bf = 0;

					AVLTree_L_Rotate(&root->bst.left);
					AVLTree_R_Rotate(node);
					break;
			}

			return 0;
		}

(7)右平衡

右平衡是左平衡的对称方向,跟左平衡一样也分为符号相同,和符号相反的两种,右平衡处理中的符号相同我就不画图了,这个比较简单,平衡处理之后bf的值都为0。
下面主要讲解符号相反的情况:

  1. Rl_node的bf=0的情况

d451e78c3fb4fa2adbf064da482d0927.png


这个情况比较简单,双旋处理完之后,root结点的bf和R_node结点的bf都等于0

  1. R_node的bf=LH

05af4a52e38d9dbf7aed278074be55f5.png


先右旋:

8ef6f735902422f846601422b9958e41.png


有旋完成之后再做左旋:

61e4aa67d357540694d66e0cc76e712e.png


左旋完成之后,root的bf=0,R_node的bf=RH。

  1. R_node的bf=RH
    最后一个了,画图:

f79eb44f1000bb3b6f4f8634fcbcf23c.png


左旋:

c6e021b3561015df8fba3fa64e491f1b.png


右旋:

f3ee92e8f4e2c930c2cb3a2dfe3232a3.png


结束之后,root的bf=0,R_node的bf=EH,代码实现:

**/**
		* @brief  AVL右平衡 (BF<0的时候的情况)
		* @param  
		* @retval 算法处理完之后,node指针已经被修改了值 
		*/ 
		int AVLTree_RightBalance(struct AVLTree_node **node)
		{
			//node为这颗最小不平衡树的根结点(node结点就是他的父结点的左右指针的一个)
			struct AVLTree_node *root = *node;

			//获取根结点的左子树 L
			struct AVLTree_node *R_node = root->bst.right;
			struct AVLTree_node *Rl_node = R_node->bst.left;

			//根结点的BF<0的时候,   它的右子树的bf   有两种情况:
			printf("AVLTree_RightBalance %dn", R_node->bst.bf);
			switch(R_node->bst.bf)
			{
				//新插入的元素是插在root的右子树上的时候,只需要左旋就好
				case RH:
					//左旋也是分两种情况,但是这两种情况的结果是一样的,根结点的bf值和L_node结点的bf值都为0
					root->bst.bf = R_node->bst.bf = EH;		
					AVLTree_L_Rotate(node);
					break;

				//如果是反向的话,就需要双旋了,双旋比较复杂有三种情况的
				case LH:
					printf("AVLTree_RightBalance LH %dn", Rl_node->bst.bf);
					switch(Rl_node->bst.bf)
					{
						case EH:
							//想等的情况最简单,记得这是处理完,双旋之后的结果,双旋之后的结果就是两个bf=0
							root->bst.bf = Rl_node->bst.bf = 0;
							break;

						//下面这两种情况,在上面的图解上已经分析过了,平衡二叉树的难点就是BF值的更新,真难
						case LH: 
							root->bst.bf = EH;
							R_node->bst.bf = RH;
							break;

						case RH:
							root->bst.bf = LH;
							R_node->bst.bf = EH;
							break;
					}

					Rl_node->bst.bf = 0;

					AVLTree_R_Rotate(&root->bst.right);
					AVLTree_L_Rotate(node);
					break;
			}

			return 0;
		}

(8)平衡二叉树删除

平衡二叉树的删除也涉及到删除后的连接问题。其删除一般分为4种情况:
1)删除叶子结点;
2)删除左子树为空,右子树不为空的结点:
3)删除左子树不为空,右子树为空的结点;
4)删除左右子树都不为空的结点。

删除叶子结点很简单,直接删除即可,此处不再赘述。接下来分别学习其他三种删除情况。

左子树为空,有子树不为空

以图中的平衡二叉树为例。
现要删除结点105,结点105有右子树,没有左子树,则删除后,只需要将其父结点与其右子树连接即可。

ba2675e842159b3e74eb797986dbc598.png


删除结点会使相应子树的高度减小,可能会导致树失去平衡,如果删除结点后使树失去平衡,要调整最小不平衡子树使整棵树达到平衡。插入和删除一样,在删除的过程中要时刻保持树的平衡性。

做子树不为空,右子树为空

要删除一个结点,结点有左子树没有右子树,这种情况与上一种情况相似,只需要将其父结点与其左子树连接即可。例如要删除图中的结点100,其删除过程如图所示:

377feaeae1b274444eb83b86b3aec76b.png

左右子树均不为空

如果要删除的结点既有左子树又有右子树,则要分情况进行讨论。

(1)如果该结点x的平衡因子为0或者1 ,找到其左子树中具有最大值的结点max,将max的内容与x的元素进行交换,则max即为要删除的结点。由于树是有序的,因此找到的max结点只可能是一个叶子结点或者一个没有右孩子的结点。

例如现在有一棵平衡二叉树。现在要删除结点20,结点20的平衡因子是1,则在其左子树中找到最大结点15,将两个结点的数值互换。

a1be5bc059cf9e430e1b0c887f1cf295.png

然后删除结点20。
在删除结点20之后,平衡二叉树失去了平衡,结点10的平衡因子为2,则需要对此最小不平衡子树进行调整,此次调整类似于插入,先进性一次左旋转再进行一次右旋转即可,调整后的结果如图:

e039c80ac6348c308389b2132317fb92.png


(2)如果要删除的结点其平衡因子为-1,则找到其右结点中具有最小值的结点min,将min与x的数据值进行互换,则min即为新的要删除的结点,将结点删除后,如果树失去了平衡,则需要重新调整。由于平衡二叉树是有序的,因此这样的结点只可能是一个叶子结点,或者是一个没有左子树的结点。


首先恭喜您,能够认真的阅读到这里,如果对部分理解不太明白,建议先将文章收藏起来,然后对不清楚的知识点进行查阅,然后在进行阅读,相应你会有更深的认知。如果您喜欢这篇文章,就点个赞或者【关注我】吧!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值