前言
在学习了二叉搜索树后,我们可以通过二叉搜索树实现结点有序存储。
我们在删除结点的时候,采用的是该结点右子树中最小结点代替他,探后再删除最小的那个结点,这种做法虽然保证了二叉树的有序性,但是久而久之使二叉树越来越不平衡,右边结点越来越少。
像这样
或许在右子树中选择最小和左子树中选择最大轮换地使用可以稍微解决一下这种不平衡,但是在这里我们介绍一种AVL树来保证二叉查找树的平衡,AVL树得名于它的发明者G.M. Adelson-Velsky和E.M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。这种数据结构算是最老的一种平衡树了,不过我们还是很有必要了解它。
AVL树的基本概念
平衡二叉查找树,也被称为高度平衡树。相比于”二叉查找树”,它的特点是:AVL树中任何节点的两个子树的高度最大差别为1。
AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。
如果在AVL树中插入或删除节点后,使得高度之差大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理。学AVL树,重点的地方也就是它的旋转算法;在后文的介绍中,再来对它进行详细介绍。
AVL树的实现
头文件的定义
#ifndef AVLTREE_H_INCLUDED
#define AVLTREE_H_INCLUDED
typedef int elementType;
typedef struct node
{
elementType key;
struct node *left;
struct node *right;
int height;//当前结点深度
} avlnode,*avltree;
int getNode_height(avlnode *node);//获取当前结点的深度
avlnode *create_node(elementType key,avlnode *left,avlnode *right);//创建结点
avlnode * maximun_node(avltree tree);//求树中最大结点
avlnode *minimun_node(avltree tree);//求树中最小结点
avltree avltree_insertNode(avltree tree,elementType key);//向树中插入结点
avltree avltree_deleNode(avltree tree,elementType key);//删除结点
void pre_order_avltree(avltree tree);//前序遍历
void in_order_avltree(avltree tree);//中序遍历
void post_order_avltree(avltree tree);//后序遍历
void print_avltree(avltree tree, elementType key, int direction);//打印树的信息
avlnode *search_node(avltree tree,elementType key);//根据key 的值搜索结点
#endif // AVLTREE_H_INCLUDED
创建结点
avlnode *create_node(elementType key,avlnode *left,avlnode *right)
{
avlnode *node=(avlnode *)malloc(sizeof(avlnode));
if(node==NULL)
{
printf("创建结点失败");
return NULL;
}
node->key=key;
node->left=left;
node->right=right;
node->height=0;
return node;
}
获取结点深度
#define HEIGHT(node) ((node==NULL) ? 0 :(((avlnode *)(node))->height))
int getNode_height(avlnode *node)
{
return HEIGHT(node);
}
比较大小
#define MAX(a,b) ((a)>(b)?(a):(b))
旋转
前面说过,如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。这种失去平衡的可以概括为4种姿态:LL(左左),LR(左右),RR(右右)和RL(右左)。下面给出它们的示意图:
上图中的4棵树都是”失去平衡的AVL树”,从左往右的情况依次是:LL、LR、RL、RR。除了上面的情况之外,还有其它的失去平衡的AVL树,如下图:
(1) LL:LeftLeft,也称为”左左”。插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致”根的左子树的高度”比”根的右子树的高度”大2,导致AVL树失去了平衡。
例如,在上面LL情况中,由于”根节点(8)的左子树(4)的左子树(2)还有非空子节点”,而”根节点(8)的右子树(12)没有子节点”;导致”根节点(8)的左子树(4)高度”比”根节点(8)的右子树(12)”高2。
(2) LR:LeftRight,也称为”左右”。插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致”根的左子树的高度”比”根的右子树的高度”大2,导致AVL树失去了平衡。
例如,在上面LR情况中,由于”根节点(8)的左子树(4)的左子树(6)还有非空子节点”,而”根节点(8)的右子树(12)没有子节点”;导致”根节点(8)的左子树(4)高度”比”根节点(8)的右子树(12)”高2。
(3) RL:RightLeft,称为”右左”。插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致”根的右子树的高度”比”根的左子树的高度”大2,导致AVL树失去了平衡。
例如,在上面RL情况中,由于”根节点(8)的右子树(12)的左子树(10)还有非空子节点”,而”根节点(8)的左子树(4)没有子节点”;导致”根节点(8)的右子树(12)高度”比”根节点(8)的左子树(4)”高2。
(4) RR:RightRight,称为”右右”。插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致”根的右子树的高度”比”根的左子树的高度”大2,导致AVL树失去了平衡。
例如,在上面RR情况中,由于”根节点(8)的右子树(12)的右子树(14)还有非空子节点”,而”根节点(8)的左子树(4)没有子节点”;导致”根节点(8)的右子树(12)高度”比”根节点(8)的左子树(4)”高2。
前面说过,如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。AVL失去平衡之后,可以通过旋转使其恢复平衡,下面分别介绍”LL(左左),LR(左右),RR(右右)和RL(右左)”这4种情况对应的旋转方法。
LL旋转
LL失去平衡的情况,可以通过一次旋转让AVL树恢复平衡。如下图:
图中左边是旋转之前的树,右边是旋转之后的树。从中可以发现,旋转之后的树又变成了AVL树,而且该旋转只需要一次即可完成。
对于LL旋转,你可以这样理解为:LL旋转是围绕”失去平衡的AVL根节点”进行的,也就是节点k2;而且由于是LL情况,即左左情况,就用手抓着”左孩子,即k1”使劲摇。将k1变成根节点,k2变成k1的右子树,”k1的右子树”变成”k2的左子树”。
LL旋转代码
static avltree left_left_rotation(avltree tree)
{
avlnode *k2=tree->left;
tree->left=k2->right;
k2->right=tree;
//!!!!切记所有旋转操作后要重新调整树的高度
tree->height=MAX(getNode_height(tree->left),getNode_height(tree->right))+1;
k2->height=MAX(getNode_height(k2->left),getNode_height(k2->right))+1;
return k2;
}
RR旋转
理解了LL之后,RR就相当容易理解了。RR是与LL对称的情况!RR恢复平衡的旋转方法如下:
RR旋转代码:
static avltree right_right_rotation(avltree tree)
{
avlnode *k3=tree->right;
tree->right=k3->left;
k3->left=tree;
tree->height=MAX(getNode_height(tree->left),getNode_height(tree->right))+1;
k3->height=MAX(getNode_height(k3->left),getNode_height(k3->right))+1;
return k3;
}
LR旋转
LR失去平衡的情况,需要经过两次旋转才能让AVL树恢复平衡。如下图:
LR旋转代码:
static avltree left_right_rotation(avltree tree)
{
tree->left=right_right_rotation(tree->left);
tree=left_left_rotation(tree);
return tree;
}
RL旋转
RL是与LR的对称情况!RL恢复平衡的旋转方法如下:
第一次旋转是围绕”k3”进行的”LL旋转”,第二次是围绕”k1”进行的”RR旋转”。
RL旋转代码:
static avltree right_left_rotation(avltree tree)
{
tree->right=left_left_rotation(tree->right);
tree=right_right_rotation(tree);
return tree;
}
插入节点
/*
插入结点操作类似二叉树搜索树,但是avl要在插入新结点后保证树的平衡性
*/
avltree avltree_insertNode(avltree tree,elementType key)
{
if(tree==NULL)
{
avlnode *node=create_node(key,NULL,NULL);
tree=node;
}
else if(key<tree->key)//在左子树中插入结点
{
tree->left=avltree_insertNode(tree->left,key);//递归寻找插入节点的位置
//插入节点后可能引起二叉树的不平衡,所以要在此进行判断
if(HEIGHT(tree->left)-HEIGHT(tree->right)==2)
{
//在这儿判断是LL还是LR
if(key<tree->left->key)
{
//LL旋转
tree= left_left_rotation(tree);
}
else
{
//LR旋转
tree=left_right_rotation(tree);
}
}
}
else if(key>tree->key)//在右子树中插入结点
{
tree->right=avltree_insertNode(tree->right,key);
if(getNode_height(tree->right)-getNode_height(tree->left)==2)
{
//RR旋转
if(key>tree->right->key)
{
tree= right_right_rotation(tree);
}
else
{
//RL旋转
tree=right_left_rotation(tree);
}
}
}
else
{
printf("不允许插入相同值结点");
}
//!!!重新调整二叉树的深度
tree->height=MAX(getNode_height(tree->left),getNode_height(tree->right))+1;
return tree;
}
打印AVL树
/*
* 打印"AVL树"
*
* tree -- AVL树的节点
* key -- 节点的键值
* direction -- 0,表示该节点是根节点;
* -1,表示该节点是它的父结点的左孩子;
* 1,表示该节点是它的父结点的右孩子。
*/
void print_avltree(avltree tree, elementType key, int direction)
{
if(tree != NULL)
{
if(direction==0) // tree是根节点
printf("%2d is root\n", tree->key, key);
else // tree是分支节点
printf("%2d is %2d's %6s child\n", tree->key, key, direction==1?"right" : "left");
print_avltree(tree->left, tree->key, -1);
print_avltree(tree->right,tree->key, 1);
}
}
搜寻AVL中结点
avlnode *search_node(avltree tree,elementType key)
{
if(tree==NULL||tree->key==key)
{
return tree;
}
else if(key<tree->key)
{
search_node(tree->left,key);
}
else
{
search_node(tree->right,key);
}
}
删除AVL树种结点操作
删除操作的调整和插入操作的调整的不同之处在于: 插入之后,我们最多只需要调整两次就可以使树恢复平衡,要么是单旋转(作为一次调整),要么是双旋转(作为两次调整);但是删除之后,我们可能做多次调整,可能做了一次单旋转之后,还需要再做一次双旋转或单旋转,原因在于插入之后,插入前的子树和插入后的子树的高度是相同的,而删除之后,删除前的子树和删除后的子树的高度可能是不同的。下面的三幅图说明了删除操作对子树高度的改变。
在上图中,可以看到,原先节点50的右子树高度为3,我们现在删除节点80,下图是删除之后对节点75单旋转之后的情况
可以看到,对节点75进行旋转之后,节点50的右子树变矮了,这个在插入的时候是不可能的,你可以想一下为什么。
正如上图所示,这个时候,根节点50不平衡了,于是我们需要进行第二次旋转,以上就是删除操作比插入操作略微麻烦的原因,但是我们只要沿着当前结点到根节点的路径,一路向上进行调整,便可以使树重新恢复平衡。
/*
删除avl中的结点 ------ 删除结点和二叉搜索树的策略类似,但关键是维护树的平衡性
*/
avltree avltree_deleNode(avltree tree,elementType key)
{
avlnode *node =search_node(tree,key);
if(tree==NULL||node==NULL)
{
return tree;
}
if(key<tree->key)//要删除的结点在左子树
{ //递归找到要删除的结点
tree->left= avltree_deleNode(tree->left,key);
//删完后要检查平衡性
if(getNode_height(tree->right)-getNode_height(tree->left)==2)
{
if(key<tree->right->key)
{
tree=right_left_rotation(tree);//RL旋转
}
else
{
tree=right_right_rotation(tree);//RL旋转
}
}
}
else if(key>tree->key) //要删除的结点在右子树
{
tree->right= avltree_deleNode(tree->right,key);
if(getNode_height(tree->left)-getNode_height(tree->right)==2)
{
if(key<tree->left->key)
{
tree=left_left_rotation(tree);//LL旋转
}
else
{
tree=left_right_rotation(tree);//LR旋转
}
}
}
else //找到要删除的结点
{
/*如果要删除的结点有两个孩子,删除策略同二叉搜索树,右子树中最小结点赋值给当前结点,
并删除最小结点,这样保证了二叉树的有序性,下面再讨论二叉树的平衡性*/
if(tree->left&&tree->right)
{
avlnode *min_node=minimun_node(tree->right);
tree->key=min_node->key;
tree->right= avltree_deleNode(tree->right,min_node);
}
else
{
tree=tree->left?tree->left:tree->right;//独子或者无子情况删除结点同儿茶搜索树
}
}
if(tree)
tree->height=MAX(getNode_height(tree->left),getNode_height(tree->right))+1;
return tree;
}
github代码下载:代码地址
参考博客: