前面说到了二叉搜索树,它在动态查找表中有较高的性能,既能保证在无序数据中查找的高效率,又相对于数组具有方便的增添删除功能,
其平均查找复杂度为所构造的二叉树的树高,即O(logn)( 图1-1 ),但是,对于极端情况,例如该二叉树是一颗左斜树或者右斜树 (图1-2) 。
此时,显然可以看到,最坏查找时间复杂度为O(n),这就意味着我们的二叉搜索树变成了顺序查找,效率极为低下,所以我们需要一种
平衡二叉树的方法来避免出现极端情况,方法有很多,典型的有AVL树,红黑树等,他们各有优势,这里说的是AVL树。(注:平衡二叉树
中每一个节点的左子树和右子树高度差至多等于 1 )
图1-1 图1-2
目前网上有两种AVL树的平衡方式,一种是树高度法,另外一种是平衡因子法,据说树高度法存在效率问题,本人目前才疏学浅,对树高度法了解
甚少,故对网上的评论不置评论,这里采用的是平衡因子的方法。既然有一个因子用来作为二叉树的平衡判断方式,那么我们的二叉树结构需作
相应修改 (注:平衡因子指当前节点的左子树高度减去右子树高度的差)
typedef struct Anode{
int data;
int bf;//即当前节点的左子树高度减去右子树高度的差
struct Anode *lchild, *rchild;
}Anode, *AVLTree;/*AVL树结构,在二叉树基础上加了一个bf平衡因子*/
AVL树的关键字查找操作与二叉搜索树类似,就不再赘述,后面直接上代码,这里主要是插入和删除操作需要做大调整。我们知道,给你一堆数据,
通过循环调用二叉搜索树的插入代码,就可以完成一颗二叉搜索树的构建,但是若要达到平衡效果,就要时时刻刻监测当前插入的节点是否破坏了
二叉搜索树的平衡性,正所谓 “把XXX扼杀在摇篮里” ,每次插入一个新节点,若破坏平衡性,就调整它,若删除一个节点破坏了树的平衡,也如此。
首先让我们看看破坏平衡的四种情况,用构建一颗AVL树来说明,假如我们给定a[10] = {3,2,1,4,5,6,7,10,9,8}来构建二叉搜索树(注:图中节点左上角
数字表示当前节点的 bf 平衡因子值)
图1 图2 图3 图4
一、在构建二叉搜索树过程中,图1 跟图2 是典型的最小不平衡子树右旋过程及结果,当我们插入 节点1 时,可以知道 节点3 的平衡因子变为了 2 ,因此需要调整,我们将从 节点3 开始的最小不平衡子树右旋 (顺时针),此时 节点2 变成了树的根节点,整棵树达到平衡状态;
二、图3 跟 图4 是最小不平衡子树左旋过程及结果,当插入 节点5 后, 节点2 和 节点3 平衡因子都变成了 –2,但是最小不平衡子树根节点是离插入节点最近的不平衡节点,所以此时的最小不平衡子树是以 节点3 为根节点的子树,将整棵树左旋 (逆时针),此时 节点4 成为新的子树根节点,子树达到平衡状态;
图5 图6
图7
三、图5、图6 跟 图7 是最小不平衡子树的双旋情况,图5若根据前面讲解左旋后 节点9 变成了 节点10 的右孩子,这就不满足二叉搜索树的定义了,故需要调整,仔细观察发现最小不平衡子树根 节点7 与其右孩子 节点10 平衡因子一正一负,而我们之前说的左旋跟右旋,最小不平衡子树根节点的平衡因子与其孩子是同符号的,所以我们可以先把以 节点10 为根节点的子树进行右旋,形成 图6 ,此时再左旋以 节点7 为根节点的子树,达到平衡状态;(注:此处节点9并无右孩子,否则应该在 节点10 右旋后作为节点10 的左孩子,而节点9 变成子树新根节点,如此才能达到平衡且与节点7 同符号以进行后续的节点7 左旋 )
图8 图9
图10
四、图8、图9 跟 图10 是最小不平衡子树双旋的另一种情况,同样当我们插入 节点8 之后,最小不平衡子树的根 节点6 平衡因子变为了 -2 ,此时应该左旋,但是其右孩子 节点9 的平衡因子与其不同符号,故我们需要先将以 节点9 为根节点的子树进行右旋,得到 图9 ,注意此时右旋由于 节点7 将作为新的子树根节点而原来 节点7 的右孩子 节点8 将划分为 节点9 的左孩子,这样才能达到平衡的效果 (可参考下图) ,之后再将以 节点6 为根节点的最小不平衡子树进行左旋,得到 图10 ,达到平衡效果。
说了很多,终于要上代码了,好累,解释可能还不够清楚,大家可以多多参考其他数据结构相关的书 ——-
/*
* 平衡二叉树(AVL):由于在极端情况下(左斜树和右斜树)
* 二叉搜索树(BST)查找时间效率大大降低到O(n),故需要
* 对其进行平衡,平衡的方法有很多种,如AVL,红黑树等,
* 此处通过平衡因子的方法使用AVL树来达到平衡的目的。
*
* 2015.12.06
* By Snow Yong
*
*/
#define LH -1 //LH,EH,RH表示平衡因子值
#define EH 0
#define RH 1
#include <stdio.h>
#include <stdlib.h>
typedef struct Anode{
int data;
int bf;//即当前节点的左子树高度减去右子树高度的差
struct Anode *lchild, *rchild;
}Anode, *AVLTree;/*AVL树结构,在二叉树基础上加了一个bf平衡因子*/
int InsertAVL(AVLTree *T, int key, int *taller);
int LeftBalance(AVLTree *T);
int RightBalance(AVLTree *T);
void LeftRotate(AVLTree *T);
void RightRotate(AVLTree *T);
int DeleteAVL(AVLTree *T, int key, int *lower);
int Delete(AVLTree *p);
int SearchAVL(AVLTree T, int key, AVLTree f, AVLTree *p);
void InOrderView(AVLTree T);
//AVL树插入关键字操作,*taller用来记录插入节点后树是否有长高
int InsertAVL(AVLTree *T, int key, int *taller)
{
if (!(*T))
{//若AVL树是空树,则直接分配节点内存存入关键字,树长高
(*T) = (AVLTree)malloc(sizeof(Anode));
(*T)->data = key;
(*T)->lchild = (*T)->rchild = NULL;
(*taller) = 1;
return 1;
}
else
{//若AVL非空树
if (key == (*T)->data)
{//如果找到当前节点的值等于欲插入的关键字,则说明已存在该关键字,不再重复插入,树没长高
(*taller) = 0;
return 0;
}
else if (key < (*T)->data)
{//若欲插入的关键字小于当前节点值,则在当前节点的左子树递归看是否插入该节点
if (!InsertAVL(&(*T)->lchild, key, taller))
{//插入失败
return 0;
}
if (*taller)
{//若插入成功,则*taller必然等于1,此时只要根据当前节点的平衡因子来调整树达到平衡
switch ((*T)->bf)
{ //插入节点在当前节点的左子树的某处
case LH: //如果原来左子树高度就比右子树高1
LeftBalance(T); //就调用左平衡函数
(*taller) = 0; //平衡过后,树相对于原来的树并无长高,故设置(*taller) = 0
break;
case EH: //如果原来左子树跟右子树等高
(*T)->bf = LH; //则当前节点的平衡因子就改为LH(左边高1)
(*taller) = 1; //相比原树,树长高
break;
case RH: //如果原来右子树高度就比左子树高1
(*T)->bf = EH; //则在左子树插入关键字节点后,当前节点平衡因子变为EH(左右等高)
(*taller) = 0; //相比原树,树并未长高
break;
}
}
}
else
{//若欲插入的关键字大于当前节点值,则在当前节点的右子树递归看是否插入该节点
//以下代码道理同上,只不过是在右子树上做文章,不再赘述
if (!InsertAVL(&(*T)->rchild, key, taller))
{
return 0;
}
if (*taller)
{
switch ((*T)->bf)
{
case LH:
(*T)->bf = EH;
(*taller) = 0;
break;
case EH:
(*T)->bf = RH;
(*taller) = 1;
break;
case RH:
RightBalance(T);
(*taller) = 0;
break;
}
}
}
return 1;
}
}
//左平衡函数,调用此函数说明当前节点的子树已然左边不平衡
int LeftBalance(AVLTree *T)
{//此函数包含了左平衡的两种情况:1.根节点单右旋 2.根节点左孩子左旋然后根节点右旋
AVLTree L, Lr;
L = (*T)->lchild; //当前树根节点的左孩子
switch (L->bf)
{
case LH: //若L左子树高,则直接根节点单右旋,并修正根节点与其左孩子的平衡因子值
(*T)->bf = L->bf = EH; //这两句代码不可调换
RightRotate(T); //因为右旋转函数会改变原来的T值(即当前树根节点发生变化)
break;
case RH:
Lr = L->rchild; //若L右子树高,则根据L的右孩子Lr的平衡因子值来修正根节点T与L的平衡因子值
switch(Lr->bf)
{//分情况修改平衡因子值,请自行画辅助图理解
case LH:
(*T)->bf = RH;
L->bf = EH;
break;
case EH:
(*T)->bf = L->bf = EH;
break;
case RH:
(*T)->bf = EH;
L->bf = LH;
break;
}
Lr->bf = EH; //平衡完后L的右孩子平衡因子修正为EH,因左右等高
LeftRotate(&(*T)->lchild); //先对根节点T的左孩子进行左旋
RightRotate(T); //再对根节点T进行右旋
break;
}
}
//右平衡函数,道理同上,代码差别如同对称成像,不再赘述,自行阅读
int RightBalance(AVLTree *T)
{
AVLTree R, Rl;
R = (*T)->rchild;
switch (R->bf)
{
case LH:
Rl = R->lchild;
switch(Rl->bf)
{
case LH:
(*T)->bf = EH;
R->bf = RH;
break;
case EH:
(*T)->bf = R->bf = EH;
break;
case RH:
(*T)->bf = LH;
R->bf = EH;
break;
}
Rl->bf = EH;
RightRotate(&(*T)->rchild);
LeftRotate(T);
break;
case RH:
(*T)->bf = R->bf = EH;
LeftRotate(T);
break;
}
}
//左旋函数
void LeftRotate(AVLTree *T)
{
AVLTree R;
R = (*T)->rchild; //取当前树根节点的右孩子
(*T)->rchild = R->lchild; //将右孩子R的左孩子作为当前节点*T的右孩子
R->lchild = (*T); //将*T作为R的左孩子
(*T) = R; //将当前节点的右孩子R作为新的树根节点
}
//右旋函数
void RightRotate(AVLTree *T)
{//与左旋函数操作相反,不再赘述
AVLTree L;
L = (*T)->lchild;
(*T)->lchild = L->rchild;
L->rchild = (*T);
(*T) = L;
}
//AVL树删除关键字操作,*lower用来记录插入节点后树是否有变矮
int DeleteAVL(AVLTree *T, int key, int *lower)
{
if (!(*T))
{//若当前树为空树,则删除失败返回0
return 0;
}
else
{
if (key == (*T)->data)
{ //若当前节点等于欲删除关键字
Delete(T); //则调用删除函数
(*lower) = 1; //树变矮
return 1;
}
else if (key < (*T)->data)
{//若欲删除关键字小于当前节点值
if (!DeleteAVL(&(*T)->lchild, key, lower))
{//如果左子树也找不到等于此关键字的节点值,删除失败
return 0;
}
if (*lower)
{//若删除成功,则*lower必然等于1,此时只要根据当前节点的平衡因子来调整树达到平衡
switch ((*T)->bf)
{ //删除的节点在当前节点的左子树的某处
case LH: //如果原来左子树高度就比右子树高1
(*T)->bf = EH; //则删除后,两边等高,故(*T)->bf = EH
(*lower) = 1; //相比原树,树变矮
break;
case EH: //如果原来左子树与右子树等高
(*T)->bf = RH; //则删除后右子树比较高
(*lower) = 0; //左边树变矮,右边不变,但总体树高没变矮
break;
case RH: //如果原来右子树高度就比左子树高1
RightBalance(T); //则删除后,右子树比左子树高2,故需要调用右平衡函数
(*lower) = 1; //调整过后,相比原树,树变矮
break;
}
}
}
else
{//若欲删除关键字大于当前节点值,道理同上,不再赘述
if (!DeleteAVL(&(*T)->rchild, key, lower))
{
return 0;
}
if (*lower)
{
switch ((*T)->bf)
{
case LH:
LeftBalance(T);
(*lower) = 1;
break;
case EH:
(*T)->bf = LH;
(*lower) = 0;
break;
case RH:
(*T)->bf = EH;
(*lower) = 1;
break;
}
}
}
return 1;
}
}
//AVL树删除节点以及后续结构调整函数
int Delete(AVLTree *p)
{//*p为当前要删除的树根节点,为了满足二叉搜索树的性质,分情况讨论
AVLTree q, s;//定义两个节点指针,q是s的父节点
if ((*p)->rchild == NULL)
{ //如果当前欲删除节点没有右子树(孩子)
q = (*p); //则直接将当前节点的左孩子作为新的树根节点
(*p) = (*p)->lchild;
free(q); //释放节点占用内存
}
else if ((*p)->lchild == NULL)
{ //道理同上
q = (*p);
(*p) = (*p)->rchild;
free(q);
}
else
{//若当前欲删除节点既有左子树又有右子树,则取此树中序遍历最接近欲删除节点的那个前驱(后继)节点作为新的树根节点
q = (*p); //将当前欲删除节点赋给q
s = (*p)->lchild; //s是当前欲删除节点的左孩子,
while (s->rchild)
{ //此处循环为了寻找欲删除节点左孩子的最右孩子,即欲删除节点的中序遍历前驱节点
q = s; //如果s有右孩子,则将s赋给q,s则替换为其右孩子,即q是s的父节点
s = s->rchild;
}
(*p)->data = s->data; //经过上述操作,s就是欲删除节点的中序遍历前驱节点,我们不是直接删除*p节点,而是
//将s的值赋给欲删除节点*p,然后删除s节点就可以达到调整的目的
if (q != (*p))
{ //若q不等于*p,说明q在while循环中被改变了,即s被替换为了其最右孩子
q->rchild = s->lchild; //删除s节点后,不保证s不含左孩子,故将其s的左孩子赋给父节点q的右孩子
}
else
{ //若q等于*p,说明while循环体里的代码没有执行,即s不含右孩子,此时
q->lchild = s->lchild; //删除s节点后,应该把s的左孩子赋给q的左孩子,不然就缺胳膊短腿了
}
free(s); //由于*p的值已被替换,结构也已调整完成,释放s节点占用内存
}
return 1;
}
//关键字查找函数,与二叉搜索树的查询函数一致,不再赘述,
//若有疑问请参考二叉搜索树(BST) ---- C语言
int SearchAVL(AVLTree T, int key, AVLTree f, AVLTree *p)
{
if (!T)
{
(*p) = f;
return 0;
}
else
{
if (key == T->data)
{
(*p) = T;
return 1;
}
else if (key < T->data)
{
return SearchAVL(T->lchild, key, T, p);
}
else
{
return SearchAVL(T->rchild, key, T, p);
}
}
}
//树的中序遍历函数(递归)
void InOrderView(AVLTree T)
{
if (!T)
{
return;
}
InOrderView(T->lchild);
printf("%d--", T->data);
InOrderView(T->rchild);
}
int main()
{
int i, taller, lower;
int a[10] = {33, 4, 12, 58, 99,
40, 67, 55, 1, 120};
AVLTree p, T = NULL;
for (i = 0; i < 10; i++)
{
InsertAVL(&T, a[i], &taller);
}
InOrderView(T);
SearchAVL(T, 55, NULL, &p);
printf("\nSearch--p: %d", p->data);
DeleteAVL(&T, 55, &lower);
puts("\n");
InOrderView(T);
SearchAVL(T, 55, NULL, &p);
printf("\nSearch--p: %d", p->data);
return 0;
}
运行结果:
AVL树的实现代码比较复杂,主要要理解平衡的概念,左平衡跟右平衡各有两种情况,总共四种情况,而左旋要看其右孩子是否含有左孩子,右旋也同理,总之,多看代码,多画辅助图,容易加深理解,能力有限,谢谢阅读!