哪里有需求,哪天博客就会更新!
比如今天晚上接到需求,立马从被子里钻出来开始更新博客!
1.实验要求
2.实验过程
总的来说,本次实验就是需要实现AVL树的查找和排序。至于AVL树是啥应该不用我多说。
即AVL树首先是一颗BST树,本篇会涉及大量的BST的插入、删除、创建等知识,强烈推荐看这篇博客,看完之后你做这个实验肯定手到拈来!。哈工大数据结构实验四——BST查找结构和折半查找的实现与比较。
其次,AVL树任意一个节点的左右子树的高度不超过1,这就是AVL树。也就是说AVL树相比BST树更平衡了,如果你看了我BST查找结构和折半查找的博客,那么你肯定知道平衡的BST树查找效率几乎和二分查找效率一样!效果很好!所以AVL树的查找效率好于BST树
所以,由于AVL树的这一个特性,导致我们和BST建立、删除、查找有点区别。我们删除、建立的时候必须保持AVL树的特性。也就是各种旋转操作。所以本次实验就是用代码来实现各种旋转操作呗。
2.1 设计AVL的左右链存储结构
之前说到AVL树本质上是一棵BST树,只不过对AVL树来说,为了在插入和删除过程中保持AVL树的平衡,我们需要对失衡节点进行旋转,为了确定失衡节点,必须知道节点的高度!因此AVL树节点除了存储双链和值之外,还要存储高度这一属性!
struct AVLNode{
int data;
AVLNode *left,*right;
int height;//节点的高度
};
2.2 AVL树的各种旋转操作
本来2.2节应该讲述AVL树的插入、删除、查找等操作,但是在进行这些操作的时候必须逐一考虑各种旋转操作,所以本节就来讲述一下旋转操作!
先了解一下各种导致AVL树失衡的节点(即该节点的左右子树的高度差超过1)的插入,总共四种情况:
- LL:在失衡节点的左子树上的左子树上插入了一个节点导致失衡。
- LR:在失衡节点的左子树上的右子树上插入了一个节点导致失衡。
- RL:在失衡节点的右子树上的左子树上插入了一个节点导致失衡。
- RR:在失衡节点的右子树上的右子树上插入了一个节点导致失衡。
接下来我们分别探讨把!
2.2.1 LL型旋转
2.2.1.1思路分析
以这张图片为例,可以看到插入11之后,导致节点25失衡,11刚好插在25的左子树的左子树上,那么我们要进行的就是LL型旋转。但是上面这张图节点12没有右儿子,体现不出一般性。
更一般的情况,我们看下面这个图
由旋转结果可以看得出,旋转方法为以失衡节点的左儿子(节点12)为圆心,失衡节点和失衡节点的左儿子为边(即12和25连接的边),顺时针旋转90度,然后让失衡节点的左儿子指向失衡节点的左儿子的右儿子,失衡节点的左儿子的右儿子指向失衡节点。我们称它为顺时针旋转,也就是右旋
2.2.1.2 代码
那么代码思路就是:让失衡节点的左儿子指向失衡节点的左儿子的右儿子,失衡节点的右儿子指向失衡节点。同时更新高度即可(看上面的图可知,高度改变的节点只有失衡节点和失衡节点的左儿子,其他节点的高度都没有改变,所以只需要更新失衡节点和失衡节点的左儿子的高度即可。)
因为LL型是一次顺时针旋转即可,所以函数名字为rrotation。(第一个r为顺时针的意思,rotation为旋转的意思)
代码如下
AVLNode *rrotation(AVLNode *r){//右旋,r是失衡的节点
AVLNode *p=r->left ;//p是失衡节点的左儿子
r->left =p->right ;//让失衡节点的左儿子指向失衡节点的左儿子的右儿子
p->right =r;//失衡节点的右儿子指向失衡节点
r->height=max(hight(r->left),hight(r->right))+1;//更新失衡节点的高度
p->height=max(hight(p->left),hight(p->right))+1;//更新失衡节点左儿子的高度
return p;//返回的是失衡节点的左儿子
}
2.2.2 RR型旋转
2.2.2.1思路分析
看完了LL型旋转,那自然看看RR旋转,直觉告诉我们,这两种旋转思路基本一样,只不过顺改为逆,逆改为顺。你现在可以试着写一下代码哦。
先看RR型旋转,也就是节点插入在失衡节的右子树的右子树上面。看图中的例子。
由旋转结果可以看得出,旋转方法为以失衡节点的右儿子(节点12)为圆心,失衡节点和失衡节点的右儿子为边(即12和25连接的边),逆时针旋转90度,然后让失衡节点的右儿子指向失衡节点的右儿子的左儿子,失衡节点的右儿子的左儿子指向失衡节点。我们称它为逆时针旋转,也就是左旋
细心的你会发现,这段话我只是原封不断的复制了一下上面LL型旋转的操作,只不过把所有的左改为右,右改为左,顺改为逆
2.2.2.2 代码
代码的执行的操作:
让失衡节点的右儿子指向失衡节点的右儿子的左儿子,失衡节点的右儿子的左儿子指向失衡节点,更新失衡节点和失衡节点的右儿子的高度即可(理由同上)。
代码:函数名命名理由:因为RR型是一次逆时针旋转即可,所以函数名字为lrotation。(第一个l为逆时针的意思,rotation为旋转的意思)
AVLNode *lrotation(AVLNode *r){//左旋 ,r为失衡节点
AVLNode *p=r->right ;//p为失衡节点的右儿子
r->right =p->left ;//让失衡节点的右儿子指向失衡节点的右儿子的左儿子
p->left =r;//失衡节点的右儿子的左儿子指向失衡节点
r->height =max(hight(r->left ),hight(r->right ))+1;//更新失衡节点的高度
p->height =max(hight(p->left ),hight(p->right ))+1;//更新失衡节点的右儿子的高度
return p;//返回的是失衡节点的右儿子
}
2.2.3 LR型旋转
2.2.3.1 思路分析
LR型旋转就是在失衡节点的左子树上的右子树上插入了一个节点导致失衡。
先看图再来分析!
以这张图为例子,失衡的节点是A,由图我们可以看到LR型旋转就是进行了两次旋转,分别是逆时针旋转和顺时针旋转。所以我们LR型旋转我们可以直接调用我们之前写的两个函数即可。
但是细心的你肯定会发现:我们之前讨论的RR型旋转(也就是逆时针),它是以失衡节点的右子树为圆心进行旋转,而图中是以失衡节点的左儿子的右儿子为圆心进行旋转(也就是说我们把失衡节点的左儿子看成新的失衡节点,然后去调用之前的RR型旋转函数)。所以旋转点不一样!这就是代码的细微之差别。而对于顺时针旋转来说,和我们之前讨论的一样,就是直接以失衡节点的左儿子为圆心进行旋转。
细微差别你发现了吗? 所以直接给代码
2.2.3.2代码
代码思路:直接把失衡节点的左儿子看成新的失衡节点去调用lrotation函数(也就是逆时针旋转),再调用rrotation(也就是顺时针旋转)
函数名说明:由于LR型旋转先逆时针旋转,后顺时针旋转,所以取名为lrrotation(l为逆,r为顺,rotation为旋转的意思)
AVLNode *lrrotation(AVLNode *r){//lr旋转,r为失衡的节点
r->left =lrotation(r->left );//把失衡节点的左儿子看成新的失衡节点去调用lrotation函数
return rrotation(r); //再调用rrotation函数
}
2.2.4 RL型旋转
2.2.4.1 思路分析
好的,又来了类比推导的时候,有了LL型推导RR型的经验,我们肯定想:LR型旋转和RL型旋转几乎一样,就是所有的左儿子改成右儿子,右儿子改成左儿子,顺改为逆,逆改为顺。 没错,就是这样。
这张图可以看出,直接先顺时针旋转,然后逆时针旋转。可以直接调用先前写的函数。
但是细心的你肯定会发现:我们之前讨论的LL型旋转(也就是顺时针),它是以失衡节点的左子树为圆心进行旋转,而图中是以失衡节点的右儿子的左儿子为圆心进行旋转(也就是说我们把失衡节点的右儿子看成新的失衡节点,然后去调用之前的LL型旋转函数)。所以旋转点不一样!这就是代码的细微之差别。而对于逆时针旋转来说,和我们之前讨论的一样,就是直接以失衡节点的右儿子为圆心进行旋转。
2.2.4.2 代码
代码思路:直接把失衡节点的右儿子看成新的失衡节点去调用rrotation函数(也就是顺时针旋转),再调用lrotation(也就是逆时针旋转)
函数名说明:由于RL型旋转先顺时针旋转,后逆时针旋转,所以取名为rlrotation(l为逆,r为顺,rotation为旋转的意思)
AVLNode *rlrotation(AVLNode *r){//rl旋转,r为失衡的节点
r->right =rrotation(r->right );//把失衡节点的右儿子看成新的失衡节点去调用rlotation函数
return lrrotation(r); //再调用lrotation函数
}
2.3 AVL插入、删除、排序、查找
2.3.1 说明:AVL的展示
在开启本节之前,先说明一下,由于实验要求我们记录旋转的类型。所以我使用了一个全局变量l,来表明每次的旋转类型,没进行一次旋转,l就更新,我就输出提示信息。l初始化为-1
void xuanzhuan(){//输出旋转的类型
if(l==1){
cout<<"进行了l旋转!"<<endl;
}
else if(l==2){
cout<<"进行了r旋转!"<<endl;
}
else if(l==3){
cout<<"进行了lr旋转!"<<endl;
}
else if(l==4){
cout<<"进行了rl旋转!"<<endl;
}
else if(l==0){
cout<<"未进行旋转!"<<endl;
}
}
同时,为了反映二叉树建立过程进行了旋转操作,不仅要提示进行了哪种类型的选择,还要时刻展示二叉树的变化。我的思路是每次执行插入/删除操作,不仅展示选择类型,还时刻展示二叉树的变化。
二叉树的展示函数如下,和之前一样,广义表展示。
void Print(AVLNode *r){// 广义表打印
if(r){
cout<<r->data ;
cout<<"(";
if(r->left ){
Print(r->left );
}
else{
cout<<"#";
}
cout<<",";
if(r->right ){
Print(r->right );
}
else{
cout<<"#";
}
cout<<")";
}
else{
cout<<"#";
}
}
大致的结果如图:
测试文件: i表示插入,d表示删除
2.3.2 AVL的插入
- 递归插入,先不用管AVL树的插入要执行什么复杂的操作,单纯先回忆一下BST的插入,就是递归插入,将插入值和节点进行比较,然后看一下插在左子树还是插在右子树上。可以参考我另一篇博客。哈工大数据结构实验四——BST查找结构和折半查找的实现与比较。
- 在BST基础上,每一步插入之后,我们就需要判断节点是否失衡了。也很好判断,比较左右子树的高度是否相差超过1。
- 失衡的话就需要然后判断是需要进行哪种旋转,调用相应的函数即可。
- 最后更新高度即可。
AVLNode *Insert(AVLNode *r,int e){//插入节点e
if(r==NULL){//插入节点
r=new AVLNode;
r->data =e;
r->left =r->right =NULL;
r->height =0;
return r;
l=0;
}
else if(r->data < e){//插入在右子树
r->right =Insert(r->right ,e);//递归插入,已经将节点插入在右子树中
//接着判断是否失衡
if(hight(r->right)-hight(r->left)>=2){//插入节点后可能引起失衡
if(hight(r->right->left)>hight(r->right->right))
l=4;//单纯标记作用,rl旋转
else{
l=1;//l旋转
}
//旋转
r=(hight(r->right->left)>hight(r->right->right))?rlrotation(r):lrotation(r);
}
}
else if(r->data > e){//插入在左子树
r->left =Insert(r->left ,e);//递归插入,已经将节点插入在左子树中
//接着判断是否失衡
if(hight(r->left )-hight(r->right )>=2){//插入的节点可能引起失衡
if(hight(r->left->left)>hight(r->left->right))
l=2;//r旋转
else{
l=3;//lr旋转
}
r=(hight(r->left->left)>hight(r->left->right))? rrotation(r):lrrotation(r) ;
}
}
r->height=max(hight(r->left),hight(r->right))+1;//更新高度
return r;
}
2.3.3 AVL的删除
其实AVL的删除也很简单,因为AVL本质上也是一颗BST树,唯一不同的就是它是平衡树。我们先不管AVL树删除多么复杂,各种旋转操作,可以单纯的把它当做BST树。BST树删除一个节点的实现和思路可以参考我另一篇博客。哈工大数据结构实验四——BST查找结构和折半查找的实现与比较。
删除完节点之后,我们就需要判断是否失衡了。(是不是和AVL插入和相似,先插再说,失衡的事儿然后考虑)失衡了就调整呗。多简单是吧。
- 先执行BST的删除操作,博客参考哈工大数据结构实验四——BST查找结构和折半查找的实现与比较。
- 然后每一步删除节点之后,我们就需要判断节点是否失衡了。也很好判断,比较左右子树的高度是否相差超过1
- 失衡的话就需要然后**判断是需要进行哪种旋转,调用相应的函数即可
- 最后更新高度即可
代码如下:
AVLNode *Delete(AVLNode *r,int e){//删除节点e
if(r==NULL)
return r;
else if(r->data < e){
r->right=Delete(r->right ,e); //递归删除
}
else if(r->data > e){
r->left =Delete(r->left , e); //递归删除
}
else{//找到当前的删除值
if(r->left && r->right ){
AVLNode *p = r->right ;
while(p->left ) p=p->left ;//找到待删除节点的右子树的最左节点
r->data = p->data ; //拿右子树最小的节点顶替被删除的节点
r->right =Delete(r->right ,p->data ); //调整
}
else{
AVLNode *p=(r->left )? r->left :r->right ;//直接继承
delete(r);
return p;
}
}
//调整
if(hight(r->left )-hight(r->right )>=2){//删除节点可能引起失衡
if(hight(r->left->left)>hight(r->left->right))
l=2;//r旋转
else{
l=3;//lr旋转
}
r=(hight(r->left->left)>hight(r->left->right))? rrotation(r):lrrotation(r) ;
}
else if(hight(r->right)-hight(r->left)>=2){//插入节点后可能引起失衡
if(hight(r->right->left)>hight(r->right->right)){
l=4;//rl旋转
}
else{
l=1;//l旋转
}
r=(hight(r->right->left)>hight(r->right->right))?rlrotation(r):lrotation(r);
}
r->height=max(hight(r->left),hight(r->right))+1;
}
2.3.4 AVL的查找
查找最简单了,递归查找,待查找的值和当前节点的值比较,看递归的去哪查找。我在另一篇博客详细详细的阐述了BST的查找,实在不想重复思想了。
给个链接:哈工大数据结构实验四——BST查找结构和折半查找的实现与比较。
AVLNode* Find(AVLNode *r,int e){//查找
if(r==NULL)
return r;
else{
if(r->data > e){
Find(r->left ,e);
}
else if(r->data < e){
Find(r->right ,e );
}
else{
return r;
}
}
}
2.3.5 AVL树的排序
排序也很简单,和递归先序遍历一模一样,详细可以参考我另一篇博客。
给个链接:哈工大数据结构实验四——BST查找结构和折半查找的实现与比较。
void paixu(AVLNode *r){//排序
if(r==NULL)
return;
else{
paixu(r->left );
cout<<r->data<<" ";
paixu(r->right);
}
}
2.3.6 用到的额外函数
int max(int a,int b){
return a>b? a :b ;
}
int hight(AVLNode *r){
return r?r->height :-1;
}
2.3.7 从文件读取测试数据创建AVL树
要用到的函数之前都写完了,所以创建AVL树就很简单了,不过分阐述。
void AVL(){
fstream in;
in.open("zuoye4.txt");
char op;
int e;
in>>op>>e;
while(op!='#'){
switch(op){
case 'i':
r=Insert(r,e);
cout<<"插入后的二叉树为:";
Print(r);
xuanzhuan();
l=0;
break;
case 'd':
AVLNode *p2;
p2=Find(r,e);
if(p2==NULL)
cout<<"想删除的节点不存在"<<endl;
else{
p2=Delete(r,e);
cout<<"删除后的二叉树为:";
Print(r);
xuanzhuan();
l=0;
}
break;
default:
break;
}
in>>op>>e;
}
in.close();
}
2.3.8 手动输入创建AVL树
void zrh(){
char op;
int e;
cout<<"请输入操作类型(i:插入,d:删除, #:输入结束)和顶点的权重"<<endl ;
cin>>op>>e;
while(op!='#'){
switch(op){
case 'i':
r=Insert(r,e);
cout<<"插入后的二叉树为:";
Print(r);
xuanzhuan();
l=0;
break;
case 'd':
AVLNode *p1;
p1=Find(r,e);
if(p1==NULL)
cout<<"想删除的节点不存在"<<endl;
else{
p1=Delete(r,e);
cout<<"删除后的二叉树为:";
Print(r);
xuanzhuan();
l=0;
}
break;
default:
break;
}
cin>>op>>e;
}
}
3.实验结果
4.求help
由于本篇很多的内容涉及大量的左、右、顺、逆,所以难免会看走了眼写错了哪个字,或者思路没捋顺,写这篇博客写的太快了,如果你发现哪里不对劲或者读的不通顺的话请联系我,非常感谢之前给我的代码提出错误的朋友们。我很多代码都是直接复制粘贴,然后排版,难免会写错。。。请朋友们发现错误提示我一下。感激不尽。