6.1 平衡二叉树
6.1.1 平衡二叉树介绍
平衡二叉树是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1.
平衡二叉树也称为AVL树。
平衡二叉树是一种高度平衡的二叉排序树,要么它是一颗空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度差绝对值不超过1。我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),那么平衡因为可以取值为-1,0,1,如果二叉树上有一个结点的平衡因子的绝对值大于1,就说明该二叉树不平衡.
距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树。
56和58和60结点就构成一个最小的不平衡树。
6.1.2 实现原理
平衡二叉树的实现原理也是比较简单的,就是在插入的时候,检查插入的结点是否破坏平衡二叉树的平衡性,如果是,就找出最小不平衡树,在保存二叉树的前提下,进行旋转操作,让旋转后的树满足平衡二叉树。
还是举我们上节课的例子:
原始数据:12, 45, 89, 127, 7, 4, 56, 60, 9,14,16,58
最后我们二叉排序树的结果:
这个图明显是左轻右重,树的层数竟然是5层,查找的效应降低了很多,我举个这里例子不极端,极端的是斜树的情况会退化到O(n),但是也能作为学习平衡二叉树的例子了。当我们学会了平衡二叉树,构建的树就会平衡很多了,下面详细说说这个平衡二叉树创建的过程:
-
插入结点12
-
接着插入结点45
-
插入个89,现在就要看bf的值了
这个时候结点12的BF=-2,左子树为0,右子树为2,所以BF=-2,-2已经打破平衡性了,所以需要旋转,看着上图明显是左轻右重,因为往左边旋转,旋转的结果就是结点12变成45的左孩子,45变成了根结点,如图:
旋转过后,是不是很符合平衡二叉树的性质。 -
接下来插入127,然后插入一个7
-
插入4,插入4的时候,平衡又被打破了,再次需要旋转
结点12的BF又再次大于2,这次是左重右轻,需要右旋,右旋就是把12的结点变成结点7的右孩子,结点7替代结点12的位置,如图:
旋转完成之后,又变回原来的平衡状态。 -
插入56
插入56的时候很平衡 -
插入60
-
插入9,14,16之后平衡二叉树再次不平衡
这个是结点7的BF为-2,左轻右重,需要左旋,左旋的结果就是把12的左子树的挂到结点7的右子树上,然后12替代7的位置,7变成12的左子树,如图:
平衡二叉树再次平衡 -
插入最后一个结点58
这次平衡再次被打破,需要旋转,如图56结点的BF=-2,左轻右重,理论上是需要左旋,但是左旋之后,结点58变成了56的左子树,60替换56的位置,如图:
效果并没有改善,这里为什么会这样,是因为60结点的BF是正的,而56结点的BF是负的,所以这种情况不能简单的旋转,需要双旋,60和58结点先有旋,
这样58和56结点的符号就是同一方向了,现在可以左旋操作了,左旋操作之后
这就是一颗平衡二叉树,并且插入完成。
6.1.3 左旋
平衡二叉树的基本操作是旋转,我们先介绍左旋,左旋的操作在上面插入的时候应该说过了,这里再详细画一个图介绍介绍:
图中的虚线圈起来是一个最小不平衡树,这个树左轻右重,需要左旋,左旋的规则:左旋时候主需要关注最小不平衡树,12结点就是这个树的根结点,14结点就是根结点的右子树为:R,13是Rl,16是Rr,下来用图来表示:
Rl结点挂在根结点的右子树上,R结点替换根结点,根结点成为R的左子树,Rr结点不动。左旋之后的结果:
左旋之后,就形成了平衡二叉树,这就是左旋结果。
突然发现讲了这么久都没讲平衡二叉树的数据结构:
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的父结点的孩子结点换了。
6.1.4 右旋
上面讲解了左旋,右旋是跟左旋是对称的关系,可以看下需要右旋时候的最小平衡树:
14结点BF=2,是这个最小不平衡树的根结点,并且BF=2,表示左重右轻,需要右旋,相关结点描述,12结点是L,9结点是Ll,13结点是Lr结点,右旋步骤:Lr结点挂载在根结点上,12结点替换根结点,原来的根结点变为12结点的右子树。其实跟左旋是对称的。
进行右旋操作:
右旋之后,最小不平衡树又平衡了,这就是算法的魅力,下面看看代码:
/**
* @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;
}
看了代码跟左旋代码是差不多的。
6.1.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 %d\n", *node, data);
}
else //如果这个结点存在,需要查找到插入的位置
{
if( data < (*node)->data ) //如果插入的data数据比结点的data小,需要从左子树寻找位置
{
AVLTree_Insert(&((*node)->bst.left), data, flag); //这个需要左子树寻找位置,记得这里是递归
//假如这里递归完成,需要调整bf值,有一下情况:
printf("lflag = %d %d\n", *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 %d\n", *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:
这个图1就是符合插入14的左孩子的时候,判断14结点的BF,BF=RH,所以符合位置3的情况,这种情况只要把14结点的BF=EH,其他结点不需要改动,就可以完成整个树的BF值的改动。
图2:
这个图2就是符合9结点插入左孩子的时候,9结点的BF=EH,这时候符合位置1的情况,这时候因为9结点添加了一个左孩子,所以BF=LH,因为这是树,所以需要反递归,把包含9结点的其他结点的BF都需要改变,所以往上反递归,12结点的BF=RH,所以符合图1的情况,所以这时候就把12结点的BF=EH了,这时候就稳定了,不需要再做修改了。
还有最后一个可能,就是这个结点已经是LH,左子树高,在插入左子树的话,就导致失去平衡了,所以这时候需要左平衡处理,这个左平衡处理要到6.1.6讲,这里就是简单的引入,整体把握平衡二叉树的插入。
6.1.6 左平衡
从上一节平衡二叉树的插入,整体了解了插入的步骤,但是还是在做左平衡的时候没有说清楚,左平衡处理的时候也有几种情况需要处理,
- 符号相同的情况
父结点没有子树的情况下,插入左孩子,导致祖父结点不平衡。
89结点是有左右子树的,这的时候在左子树上插入一个结点,会导致89结点的树失去平衡。
这两种情况是属于符号相同的时候的,因为是符号相同,所以只需要简单的右旋即可。
- 符号相反的情况
这种情况是最简单的,root结点和L_node结点的bf都=0
这个就比较难了,因为谁都有子树的情况下,这种情况我画了图在旋转了:
这个就是上面的图左旋之后的结果,因为都挂着子树,想象力不够的话,还是画图吧,下一步是右旋:
右旋之后整个数就是平衡的了,从图中可以看出,以前的根结点的bf=0,以后的L_node的bf=LH了。
下面继续这种情况:
想象力不够,画图来解析,先左旋:
接下来右旋:
图画的是有点丑了,但是看树的高度也可以明白,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 %d\n", 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;
}
6.1.7 右平衡
右平衡是左平衡的对称方向,跟左平衡一样也分为符号相同,和符号相反的两种,右平衡处理中的符号相同我就不画图了,这个比较简单,平衡处理之后bf的值都为0。
下面主要讲解符号相反的情况:
-
Rl_node的bf=0的情况
这个情况比较简单,双旋处理完之后,root结点的bf和R_node结点的bf都等于0 -
R_node的bf=LH
先右旋:
有旋完成之后再做左旋:
左旋完成之后,root的bf=0,R_node的bf=RH。 -
R_node的bf=RH
最后一个了,画图:
左旋:
右旋:
结束之后,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 %d\n", 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 %d\n", 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;
}
6.1.8 平衡二叉树删除
平衡二叉树删除因为要考虑的情况较多,目前还不是很清楚,以后清楚了在回来补。