前言
之前看Nginx的红黑树结构,发现对平衡二叉树的相关内容基本都忘光了。当然,以前也就摸过皮毛,所以再学习学习。
AVL树
平衡二叉树,一种高度平衡的二叉排序树,它要么是一颗空树,要么每一节点的左子树左子树与右子树高度差绝对值至多为1。
因此,判断一刻平衡二叉树,两点:
- 首先是二叉排序树;
- 每一节点的左右子树深度只差的绝对值不超过1,这个差值我们称为平衡因子BF(Balance Factor);
距离插入节点最近的,且平衡因子绝对值大于1的节点为根的子树,称为
最小不平衡子树。
用书中图表示下:
新插入节点为37,则以58为根节点的子树即为最小不平衡子树。
AVL树实现原理
基本思想:在构建二叉排序树的过程中,每当插入一个节点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。然后,在保持二叉排序树特性的前提下,调整最小不平衡子树中各节点之间的连接关系,进行对应旋转,使之成为新的平衡子树。
调整不平衡子树的对应情况如下:(左上角为临时的BF值)
- a) BF值均为正,将树右转
- b) BF值均为负,将树左转
- c) BF值有正有负即不统一,则先旋转为统一,再重复a) 或 b),即双旋转
AVL树实现算法
先定义下AVL树节点结构以及预定宏:#define status int
#define TRUE 1
#define FALSE 0
#define LH +1 /* 左高 */
#define EH 0 /* 等高 */
#define RH -1 /* 右高 */
typedef struct avl_node_s avl_node_t;
struct avl_node_s
{
int data;
int bf;
avl_node_t *lchild;
avl_node_t *rchild;
};
向右旋转方法:
/* 参考向右旋转图 */
void avl_right_rotate(avl_node_t **P)
{
avl_node_t *L = NULL;
/* L为向右旋转后的根节点 */
L = (*P)->lchild;
(*P)->lchild = L->rchild;
L->rchild = (*P);
*P = L;
}
此源码即为右旋操作,参考上图a),意为传入一个二叉排序树p,将其左孩子节点定义为L,将L的右子树变为P的左子树,再将P改成L的右子树,最后L替换P作为根节点。
同理,向左旋转方法:
void avl_left_rotate(avl_node_t **P)
{
avl_node_t *R = NULL;
R = (*P)->rchild;
(*P)->rchild = R->lchild;
R->lchild = (*P);
*P = R;
}
下面是左旋转保持平衡的代码:
/* 算法结束时,指针P指向新的根节点 */
void avl_left_balance(avl_node_t **P)
{
avl_node_t *L = NULL;
avl_node_t *Lr = NULL;
L = (*P)->lchild;
switch (L->bf)
{
case LH: // 这里相当于上面的右转方法
(*P)->bf = L->bf = EH;
avl_right_rotate(P);
break;
case RH: // 这里相当于BF不统一,原理同上面的双旋转
Lr = L->rchild;
switch (Lr->bf) // 这里用于修改传入的根节点P及其左孩子的平衡因子,不是很好理解,具体下面用图解释~
{
case LH: // 下图(a)
(*P)->bf = RH;
L->bf = EH;
break;
case EH: // 下图(b)
(*P)->bf = L->bf = EH;
break;
case RH: // 下图(c)
(*P)->bf = EH;
L->bf = LH;
break;
}
Lr->bf = EH;
avl_left_rotate(&(*P)->lchild);
avl_right_rotate(P);
}
}
这段代码最不好理解的就是双旋转时,对于根节点及其左孩子最后的BF值修改。这里头必须自己先理清楚旋转之后的树结构,然后才能定下BF值。另外这有一个很重要的前提,就是必须保证
根节点P是最小不平衡子树的根。好吧,我是花了很久时间去理解这个了。如下面些图:
- 情形(a)
- 情形(b)
- 情形(c)
void avl_right_balance(avl_node_t **P)
{
avl_node_t *R = NULL;
avl_node_t *Rl = NULL;
R = (*P)->rchild;
switch (R->bf)
{
case RH:
(*P)->bf = R->bf = EH;
avl_left_rotate(P);
break;
case LH:
Rl = R->lchild;
switch (Rl->bf)
{
case LH:
(*P)->bf = EH;
R->bf = RH;
break;
case EH:
(*P)->bf = R->bf = EH;
break;
case RH:
(*P)->bf = EH;
R->bf = LH;
break;
}
Rl->bf = EH;
avl_right_rotate(&(*P)->rchild);
avl_left_rotate(P);
break;
}
}
下面,就是构建一棵AVL树的主函数:
/*
* 创建一棵新AVL树及插入新节点。
* P为需创建或需插入节点的AVL树,e为插入元素,taller反映P是否”增高“
*/
status avl_node_insert(avl_node_t **P, int e, status *taller)
{
if (!(*P))
{
/* 空树,开始创建一颗新AVL树 */
*P = (avl_node_t *)malloc(sizeof(avl_node_t));
(*P)->data = e;
(*P)->lchild = (*P)->rchild = NULL;
(*P)->bf = EH;
*taller = TRUE;
}
else
{
if (e == (*P)->data)
{
/* AVL树中已存在内容为e的节点,直接返回,不作插入操作 */
*taller = FALSE;
return FALSE;
}
if (e < (*P)->data)
{
/* 在P的左子树中做插入操作 */
if (!avl_node_insert(&(*P)->lchild, e, taller)) // 未插入,则退出
{
return FALSE;
}
if (*taller) /* 已插入到P中左子树中,并且树“增高” */
{
switch ((*P)->bf)
{
case LH: // 原本左子树就比右子树高,需做左平衡操作
avl_left_balance(P);
*taller = FALSE;
break;
case EH: // 原本左右子树等高
(*P)->bf = LH;
*taller = TRUE;
break;
case RH: // 原本左子树比右子树低,现在则等高
(*P)->bf = EH;
*taller = FALSE;
break;
}
}
}
else
{
/* 在P的右子树中做插入操作,原理同左子树 */
if (!avl_node_insert(&(*P)->rchild, e, taller))
{
return FALSE;
}
if (*taller)
{
switch ((*P)->bf)
{
case LH:
(*P)->bf = EH;
*taller = FALSE;
break;
case EH:
(*P)->bf = RH;
*taller = TRUE;
break;
case RH:
avl_right_balance(P);
*taller = FALSE;
break;
}
}
}
}
return TRUE;
}
测试函数,具体的打印函数我就不写了,略懒。。用gdb等调试工具跟一下,就知道没什么问题。我已经单步跟了,确保没有问题。
void main()
{
int i;
int a[10] = {3, 2, 1, 4, 5, 6, 7, 10, 9, 8};
avl_node_t *P = NULL;
status taller;
for (i = 0; i < 10; i++)
{
avl_node_insert(&P, a[i], &taller);
}
printf("Root's data == %d !\n", P->data);
}
总结
总结的话,就AVL树的内容而言没什么特别难的地方,就是每插入一个节点都需要确保保持自平衡。但就实现的算法而言,真正理解起来还是有点难度的,尤其是对于平衡因子的修改的那些代码,确实花了很多时间。
主要参考
《大话数据结构》