您的位置:首页 > 理论基础 > 数据结构算法

AVL树的构造实现

2015-08-13 13:24 148 查看
今天接触的是平衡二叉查找树,对于AVL树来说相比之前的数据结构稍微多了一些需要注意的地方,照例在此总结总结让自己巩固一二。

首先是使用AVL树的动机:

我们知道AVL树本质上是排序二叉树的一类特例,回顾一下,对于排序二叉树的定义考虑使用归纳定义:

①、空树是二叉查找树;

②、若p和q都是二叉查找树,而root是一个“关键字大于p上所有结点的关键字,并小于q上所有结点的关键字”的元素,则以root为根结点,p为左子树,q为右子树构成的二叉树是二叉查找树。

所以对于排序二叉树来说,有可能会出现这样一种情况:



此时的排序二叉树和单链表以及没有什么区别(极可能造成更大的内存浪费),而我们所期望的排序二叉树的形态应该是这样的:



实际上来说由于层序遍历的原理,越是偏向于第一种(层数越接近n)的二叉树形式,对于查找、插入和删除的时间耗费就越久。而第二种层数为log2
n左右的高度的二叉树结构是我们所希望的。

为了构建这种理想的二叉搜索树,我们定义了AVL树。

AVL树的定义也是由归纳定义完成:

①、空树是AVL树;

②、若p和q都是二叉查找树,而root是一个“关键字大于p上所有结点的关键字,并小于q上所有结点的关键字”的元素,并且p和q的高度差不超过1,则以root为根结点,p为左子树,q为右子树构成的二叉树是AVL树。

比起排序二叉树来说,AVL树仅仅对结点左右子树的高度作了要求,所以对于之前的查找,删除这些操作来说,对于之前二叉树的操作是完全一致的,不同就在于对于插入的每一次的操作,都多了一个判定高度差是否小于2和相应的平衡调整操作。

对于维持AVL树的平衡我们使用旋转(rotation)操作,这里引用一个blog,里面有详细的关于AVL树的旋转图解:

AVL树的旋转图解

由于多了这个维持动态平衡的操作,我们在定义AVL树时也略有不同:

typedef int T
typedef struct AVLtree
{
T value;
T Height;
AVLtree* left;
AVLtree* right;
}AVLtree;
static T Get_H(AVLtree* tree); //获取结点的平衡度
在结构体之中多定义一个高度的关键字并实现获取高度的函数,来实现之后的对比。旋转操作中由于对称性,这里取LL和RL两种情况(在引用的链接中对四种情况有详细说明)来实现算法:
AVLtree* LL_rotation(AVLtree* tree) //插入左子树的左叶子的情况
{
AVLtree* temp;

temp=tree->left;
tree->left=temp->right;
temp->right=tree;

tree->Height=max(Get_H(tree->left),Get_H(tree->right))+1;
temp->Height=max(Get_H(temp->left),Get_H(temp->left))+1;

return temp;
}
AVLtree* RR_rotation(AVLtree* tree);
AVLtree* RL_rotation(AVLtree* tree) //递归调用,先LL再RR

{
tree->right=LL_rotation(tree->right);
return RR_rotation(tree);
}
为了验证算法的正确性,最后采用随机数组输入构造二叉树与中序遍历输出二叉树的方式,因为由排序二叉树的性质我们可以知道中序遍历输出是有序的。最后注意一下内存管理,delete一番即可。po上杂七杂八的总实现的代码,原理已经阐述的很清楚了,code可看可不看。
#include<iostream>
#include<algorithm>
#include<time.h>
using namespace std;

typedef int T;

typedef struct AVLtree
{
T value;
T Height;
AVLtree* left;
AVLtree* right;
}AVLtree;

static T max(T a,T b)
{
if(a>b)
{
return a;
}
else
{
return b;
}
}
static T Get_H(AVLtree* tree) //获取结点的平衡度(之后与2作对比来确定旋转方式)
{
if (NULL==tree)
return -1;

return tree->Height;
}

//旋转的实现
AVLtree* LL_rotation(AVLtree* tree)
{
AVLtree* temp;

temp=tree->left;
tree->left=temp->right;
temp->right=tree;

tree->Height=max(Get_H(tree->left),Get_H(tree->right))+1;
temp->Height=max(Get_H(temp->left),Get_H(temp->left))+1;

return temp;
}
AVLtree* RR_rotation(AVLtree* tree)
{
AVLtree* temp;

temp=tree->right;
tree->right=temp->left;
temp->left=tree;

tree->Height=max(Get_H(tree->left),Get_H(tree->right))+1;
temp->Height=max(Get_H(temp->right),Get_H(temp->left))+1;

return temp;
}
AVLtree* LR_rotation(AVLtree* tree) //递归调用,先RR再LL
{
tree->left=RR_rotation(tree->left);
return LL_rotation(tree);
}
AVLtree* RL_rotation(AVLtree* tree) //递归调用,先LL再RR

{
tree->right=LL_rotation(tree->right);
return RR_rotation(tree);
}

//插入新结点的实现

AVLtree* AVLinsert(T x,AVLtree* tree)
{
if(tree==NULL)
{
tree=(AVLtree*)malloc(sizeof(AVLtree)); //申请内存空间,malloc需要的头文件是algorithm.h
tree->value=x;
tree->Height=0;
tree->left=tree->right=NULL;
}

else if(tree->value>x) //由二叉搜索树性质知此时插入左子树
{
tree->left=AVLinsert(x,tree->left);

if(Get_H(tree->left)-Get_H(tree->right)==2)
{
if((tree->left)->value>x) //还是插入左边,则进行LL旋转
{
tree=LL_rotation(tree);
}
else //否则进行LR操作
{
tree=LR_rotation(tree);
}
}
}

else if(tree->value<x) //由二叉搜索树性质知此时插入右子树
{
tree->right=AVLinsert(x,tree->right);

if(Get_H(tree->right)-Get_H(tree->left)==2)
{
if((tree->right)->value<x) //还是插入右边,则进行RR旋转
{
tree=RR_rotation(tree);
}
else //否则进行RL操作
{
tree=RL_rotation(tree);
}
}
}

tree->Height=max(Get_H(tree->left),Get_H(tree->right))+1;
return tree;
}

void AVLprint(AVLtree* tree) //为求有序,按中序遍历输出AVL树
{
if(tree->left!=NULL)
{
AVLprint(tree->left);
}
cout<<tree->value<<" ";
if(tree->right!=NULL)
{
AVLprint(tree->right);
}
}

void AVLdelete(AVLtree** t) //释放内存空间
{
if (NULL == t || NULL == *t)
return;

AVLdelete(&((*t)->left));
AVLdelete(&((*t)->right));
free(*t);
*t = NULL;
}

int main() //随机数组验证AVL树的实现
{
T i,temp;
AVLtree* tree=NULL;
srand(time(NULL));
for (i=0;i<10;++i)
{
temp=rand();
cout<<temp<<endl;
tree=AVLinsert(temp,tree);
}
cout<<endl;
AVLprint(tree);
AVLdelete(&tree);
return 0;
}
验证的结果也如下所示:



对于AVL树的学习Get到几个挺意外的point,首先是对于二叉树的数据结构——链表的理解本来应该先行熟稔了才能方便之后的学习,但是我却是在接触旋转算法的流程之中才越发感觉对链表的运作方式感觉到理解透彻;之后是我一直习惯的代码风格也有所调整,比如面对长串的掺杂变量、运算符和数字的判断句的写法方面也不知不觉做了一些微妙的变动;而贯穿整个二叉树学习曲线的递归思想也让我在越来越感到其中的便利的同时,思考其中可能的弊端与其他的出路。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息