AVL树维基百科:http://zh.wikipedia.org/wiki/AVL树
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G.M. Adelson-Velsky和E.M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。
原理请看上面维基百科词条,可以参考严蔚敏数据结构或其它书籍,这里就不对原理做过多解释了,下面将直接给出其实现,代码有详细注释。
1、基本约定
使用平衡二叉树就是为了高效的查找,一般是根据关键字查找记录,而记录一般是复杂的类型对象。这里我们以一个Student类作为记录类型,学号作为关键字。
我们假定所使用的元素类型,都能进行各种比较和赋值。用LH,EH,RH分别表示左子树高,等高,右子树高,即平衡因子-1、0、1。
#define LH +1 //左高
#define EH 0 //等高
#define RH -1 //右高
#define EQ(a,b) ((a) == (b))
#define LT(a,b) ((a) < (b))
#define LQ(a,b) ((a) <= (b))
//结点元素类型
typedef struct Student
{
int key;
string major;
Student(){}
Student(int k,string s) : key(k), major(s){}
}ElementType;
ostream& operator<<(ostream& out, const Student& s)
{
out<<"("<>(istream& in,Student& s)
{
in>>s.key>>s.major;
}
typedef int KeyType;//关键字类型
typedef struct AVLNode
{
ElementType data;
int bf;
struct AVLNode* lchild;
struct AVLNode* rchild;
AVLNode(){}
AVLNode(ElementType& e, int ibf=EH, AVLNode* lc=NULL, AVLNode* rc=NULL)
: data(e), bf(ibf), lchild(lc),rchild(rc){}
}AVLNode, *AVL;
2、初始化、销毁
/*
*Description: 初始化(其实可以不用)
*/
void initAVL(AVL& t)
{
t = NULL;
}
/*
*Description: 销毁平衡二叉树
*/
void destroyAVL(AVL& t)
{
if(t)
{
destroyAVL(t->lchild);
destroyAVL(t->rchild);
delete t;
t = NULL;
}
}
3、遍历和查找
//前序遍历
void preOrderTraverse(AVL t)
{
if(t)
{
cout<data<<" ";
preOrderTraverse(t->lchild);
preOrderTraverse(t->rchild);
}
}
//中序遍历
void inOrderTraverse(AVL t)
{
if(t)
{
inOrderTraverse(t->lchild);
cout<data<<" ";
inOrderTraverse(t->rchild);
}
}
//以前序和中序输出平衡二叉树
void printAVL(AVL t)
{
cout<<"inOrder: "<data.key))
return t;
else if LT(key,t->data.key) /* 在左子树中继续查找 */
return searchAVL(t->lchild,key);
else
return searchAVL(t->rchild,key); /* 在右子树中继续查找 */
}
左旋和右旋,大家记住“左逆右顺”就可以了。
(1)左旋-逆时针旋转(如RR型就得对根结点做该旋转)
/*
Description:
对以*p为根的二叉排序树作左旋处理,处理之后p指向新的树根结点,即旋转
处理之前的右子树的根结点。也就是书上说说的RR型.
*/
void L_Rotate(AVLNode* &p)
{
AVLNode * rc = NULL;
rc = p->rchild; //rc指向p的右子树根结点
p->rchild = rc->lchild;//rc的左子树挂接为p的右子树
rc->lchild = p;
p = rc; //p指向新的根结点
}
(2)右旋-顺时针旋转(如LL型就得对根结点做该旋转)
/*
Description:
对以*p为根的二叉排序树作右旋处理,处理之后p指向新的树根结点,即旋转
处理之前的左子树的根结点。也就是书上说说的LL型.
*/
void R_Rotate(AVLNode* &p)
{
AVLNode * lc = NULL;
lc = p->lchild; //lc指向p的左子树根结点
p->lchild = lc->rchild; //lc的右子树挂接为p的左子树
lc->rchild = p;
p = lc; //p指向新的根结点
}
所谓左平衡处理,就是某一根结点的左子树比右子树过高,从而失去了平衡。
(1)插入时如果需要左平衡处理,根结点左子树根平衡因子只可能为LH和RH。
(2)删除和插入不同,根结点左子树根的平衡因子三种情况都可能出现,因为是删除根结点右子树中的结点从而引起左子树过高,在删除前,根结点左子树根的平衡因子是可以为EH的,此种情况同样是对根结点做简单右旋处理。
/*对以指针t所指结点为根的二叉树作左平衡旋转处理
包含LL旋转和LR旋转两种情况
平衡因子的改变其实很简单,自己画图就出来了
*/
void leftBalance(AVLNode* &t)
{
AVLNode* lc = NULL;
AVLNode* rd = NULL;
lc = t->lchild;
switch(lc->bf)
{
case LH: //LL旋转
t->bf = EH;
lc->bf = EH;
R_Rotate(t);
break;
case EH: //deleteAVL需要,insertAVL用不着
t->bf = LH;
lc->bf = RH;
R_Rotate(t);
break;
case RH: //LR旋转
rd = lc->rchild;
switch(rd->bf)
{
case LH:
t->bf = RH;
lc->bf = EH;
break;
case EH:
t->bf = EH;
lc->bf = EH;
break;
case RH:
t->bf = EH;
lc->bf = LH;
break;
}
rd->bf = EH;
L_Rotate(t->lchild);//不能写L_Rotate(lc);采用的是引用参数
R_Rotate(t);
break;
}
}
类似左平衡处理,所谓右平衡处理,就是某一根结点的右子树比左子树过高,从而失去了平衡。
(1)插入时如果需要右平衡处理,根结点右子树根平衡因子只可能为LH和RH。
(2)删除和插入不同,根结点右子树根的平衡因子三种情况都可能出现,因为是删除根结点左子树中的结点从而引起右子树过高,在删除前,根结点右子树根的平衡因子是可以为EH的,此种情况同样是对根结点做简单左旋处理。
/*对以指针t所指结点为根的二叉树作右平衡旋转处理
包含RR旋转和RL旋转两种情况
*/
void rightBalance(AVLNode* &t)
{
AVLNode* rc = NULL;
AVLNode *ld = NULL;
rc = t->rchild;
switch(rc->bf)
{
case LH: //RL旋转
ld = rc->lchild;
switch(ld->bf)
{
case LH:
t->bf = EH;
rc->bf = RH;
break;
case EH:
t->bf = EH;
rc->bf = EH;
break;
case RH:
t->bf = LH;
rc->bf = EH;
break;
}
ld->bf = EH;
R_Rotate(t->rchild);//不能写R_Rotate(rc);采用的是引用参数
L_Rotate(t);
break;
case EH: //deleteAVL需要,insertAVL用不着
t->bf = RH;
rc->bf = LH;
L_Rotate(t);
break;
case RH: //RR旋转
t->bf = EH;
rc->bf = EH;
L_Rotate(t);
break;
}
}
在插入一个元素时,总是插入在一个叶子结点上。我们采用递归插入,也就是不断搜索平衡二叉树,找到一个合适的插入点(当然相同关键字不插入)。插入后,引起的第一个不平衡的子树的根结点,一定是在查找路径上离该插入点最近的,注意看代码中递归后的回溯。
/*
若在平衡的二叉排序树t中不存在和e有相同关键字的结点,则插入一个
数据元素为e的新结点,并返回true,否则返回false。若因插入而使二叉排序树
失去平衡,则作平衡旋转处理,布尔变量taller反映t长高与否
*/
bool insertAVL(AVL& t, ElementType& e, bool& taller)
{
if(t == NULL)
{
t = new AVLNode(e); //插入元素
taller = true;
}
else
{
if(EQ(e.key, t->data.key)) //树中已含该关键字,不插入
{
taller = false;
return false;
}
else if(LT(e.key, t->data.key))//在左子树中查找插入点
{
if(!insertAVL(t->lchild, e, taller))//左子树插入失败
{
return false;
}
if(taller) //左子树插入成功,且左子树增高
{
switch(t->bf)
{
case LH: //原来t的左子树高于右子树
leftBalance(t); //做左平衡处理
taller = false;
break;
case EH: //原来t的左子树和右子树等高
t->bf = LH; //现在左子树比右子树高
taller = true; //整棵树增高了
break;
case RH: //原来t的右子树高于左子树
t->bf = EH; //现在左右子树等高
taller = false;
break;
}
}
}
else //在右子树中查找插入点
{
if(!insertAVL(t->rchild, e, taller))//右子树插入失败
{
return false;
}
if(taller) //右子树插入成功,且右子树增高
{
switch(t->bf)
{
case LH: //原来t的左子树高于右子树
t->bf = EH;
taller = false;
break;
case EH: //原来t的左子树和右子树等高
t->bf = RH;
taller = true;
break;
case RH: //原来t的右子树高于左子树
rightBalance(t);//做右平衡处理
taller = false;
break;
}
}
}
}
return true; //插入成功
}
删除和插入不同的是,删除的结点不一定是叶子结点,可能是树中的任何一个结点。前面在讲解二叉查找树时,我们知道删除的结点可能有三种情况:(1)为叶子结点,(2)左子树或右子树有一个为空,(3)左右子树都不空。对第三种情况的处理我们介绍了三种处理方式,这里我们采用删除前驱的方式。注意到我们仍然采用的是递归删除,然后判断删除后树是否“变矮”了,然后进行相应的处理。对(1)(2)中情况,很好处理,树的确是“变矮”了。对于第(3)种情况,我们不能直接找到前驱结点,然后把数据拷贝到原本要删除的根结点,最后直接删除前驱结点。因为这么做,我们无法判断原先根结点子树高度的变化情况。所以我们在找到前驱结点后,不是直接删除,而是采用在根结点左子树中递归删除前驱的方式。
/*
若在平衡的二叉排序树t中存在和e有相同关键字的结点,则删除之
并返回true,否则返回false。若因删除而使二叉排序树
失去平衡,则作平衡旋转处理,布尔变量shorter反映t变矮与否
*/
bool deleteAVL(AVL& t, KeyType key, bool& shorter)
{
if(t == NULL) //不存在该元素
{
return false; //删除失败
}
else if(EQ(key, t->data.key)) //找到元素结点
{
AVLNode* q = NULL;
if(t->lchild == NULL) //左子树为空
{
q = t;
t = t->rchild;
delete q;
shorter = true;
}
else if(t->rchild == NULL) //右子树为空
{
q = t;
t = t->lchild;
delete q;
shorter = true;
}
else //左右子树都存在,
{
q = t->lchild;
while(q->rchild)
{
q = q->rchild;
}
t->data = q->data;
deleteAVL(t->lchild, q->data.key, shorter); //在左子树中递归删除前驱结点
}
}
else if(LT(key, t->data.key)) //左子树中继续查找
{
if(!deleteAVL(t->lchild, key, shorter))
{
return false;
}
if(shorter)
{
switch(t->bf)
{
case LH:
t->bf = EH;
shorter = true;
break;
case EH:
t->bf = RH;
shorter = false;
break;
case RH:
rightBalance(t); //右平衡处理
if(t->rchild->bf == EH)//注意这里,画图思考一下
shorter = false;
else
shorter = true;
break;
}
}
}
else //右子树中继续查找
{
if(!deleteAVL(t->rchild, key, shorter))
{
return false;
}
if(shorter)
{
switch(t->bf)
{
case LH:
leftBalance(t); //左平衡处理
if(t->lchild->bf == EH)//注意这里,画图思考一下
shorter = false;
else
shorter = true;
break;
case EH:
t->bf = LH;
shorter = false;
break;
case RH:
t->bf = EH;
shorter = true;
break;
}
}
}
return true;
}
(1)在平衡二叉树(AVL)插入和删除详解(下)中给出测试代码和测试用例,并给出了一个涵盖所有情况的图例。
平衡二叉树的插入和删除详解(下): http://blog.csdn.net/sysu_arui/article/details/7906303
(2)也许大家难以理解的是插入和删除过程中,平衡因子的变化。其实,插入和删除都是递归进行的,平衡因子的变化是在递归回溯过程中,自底向上改变的,至于怎么改变,把几种情况弄清楚之后,画图就出来了。
(3)另一种简单的C++实现,不用计算平衡因子:http://blog.csdn.net/sysu_arui/article/details/7921498
(4)建议看看二叉查找树的插入和删除,对比分析一下:http://blog.csdn.net/sysu_arui/article/details/7865864
参考资料:
[1]严蔚敏 数据结构(C语言版)
[2]一篇博客文章:http://blog.sina.com.cn/s/blog_66aeefeb010127ht.html (注意文章中,插入操作有点错误)
[3]另一种C实现:http://ishare.iask.sina.com.cn/f/25570459.html?retcode=0