1、AVL树的基本概念:
AVL树又称为平衡二叉排序(搜索)树,AVL树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 “An algorithm for the organization of information” 中发表了它。分解开来看:AVL树是一棵二叉树进一步是一棵二叉排序树、并且AVL树涉及平衡因子这个概念。节点的平衡因子是它的右子树与左子树的高度(深度)之差 。如果一个节点的平衡因子为-1、0或1(即平衡因子绝对值不大于1) 则该节点被认为是平衡的;而对于一棵树,任意一个节点都是平衡的那么这棵树也是平衡的。在我们向AVL树中插入一个节点时,可能会打破该树的平衡性,所以就需要重启对其进行平衡操作。
举个简单的例子:
在未进行插入操作以前,对于节点45来说,其左子树高度为2,右子树为1,平衡因子绝对值等于1(之后的均用左右子树之差的绝对值表示平衡因子,不需再强调),所以此时是平衡的。但是对于第一种插入情况:插入的值小于根节点,因此插入了高度本就大的左子树中,平衡因子绝对值被增大到2因此不再是平衡二叉排序树。
2、引起AVL树的失衡的四种情况:
既然AVL树的平衡会被打破,那么我们需要考虑的就是恢复AVL树的平衡,关于平衡的恢复有四种情况,分为单旋转两种和双旋转两种。所谓单旋转就是只需要旋转一次,而双旋转则是需要旋转两次。为什么会有单双之分?旋转的过程又是怎样的?首先来看会引起失衡的四种情况(每种情况的标题是之后会用到的解决方案):
(1)、单向右旋平衡处理(RR):
插入节点导致左边过重,向“右”平衡(旋转)。如图,插入新节点后失重(丢失平衡):
(插入10(小于18)与插入20(大于18但小于32)属于同一种情况RR,之后我们会以插入10为例分析旋转算法)
(2)、单向左旋平衡处理(LL):
同样,60(左)和80(右)对于旋转来说也是一样的。
(3)、双向旋转平衡处理(LR先左后右):
根节点的左子树的右子树也有两种情况,但都是一种解决方法,我们以右插为例。
(4)、双向旋转平衡处理(RL先右后左):
根节点的右子树的左子树也有两种情况,我们以左插为例。
3、解决失衡的妙招——旋转:
既然已经了解了会引起失衡的原因以及相关情况,我们就可以根据这四种情况来分析相关的旋转算法了(由于单旋转RR与LL思想是相似的,而双旋转RL和LR的思想也是相似的,所以我们分别以RR和LR做重点分析):
(1)、单旋转RR:
(2)、双旋转LR:
为什么向左子树的左子树中插入是单旋转,而往左子树的右子树中插入是双旋转呢?其实很简单:一次旋转之后达不到平衡(因为对LR的情况来说,新节点插入在右子树时,若还采用原有的单旋转会将本来的一边失重情况转到另一边失重,达不到目的),所以想到去再旋转一次(就像求表达式极限时,用一次洛必达法则求不出,就会想到对第一次的结果再用一次洛必达法则而去求二阶导)。但是这里的二次旋转并不是对同一个根节点旋转两次(右旋转第一次平衡不了,对根节点再左旋转一次会恢复到最初的不平衡状态,大家可以自己试一试就知道了),那么我们看看LR的实际旋转过程:
而RL这跟LR也是相似的,只需将遇到right的地方换成left,遇到left的地方换成right即可。
4、插入算法的测试代码:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
typedef struct _node{
int data;
int high;//记录当前节点深度
struct _node* left;
struct _node* right;
}NODE;
/*创建新节点*/
NODE *construct_node(int data)
{
NODE * tmp = (NODE *)malloc(sizeof(NODE));
memset(tmp, 0, sizeof(NODE));
tmp->high = 0;//新节点高度为0
tmp->data = data;
tmp->left = tmp->right = NULL;
return tmp;
}
/*中序遍历*/
void inorder_traversal(NODE * root)
{
if(!root)
return ;
NODE * tmp = root;
if(tmp){
inorder_traversal(tmp->left);
printf("%d(%d) ",tmp->data, tmp->high);//打印深度可以不打印,因为有中序与先序可以还原之后再判断效果
inorder_traversal(tmp->right);
}
}
/*先序遍历*/
void preorder_traversal(NODE * root)
{
if(!root)
return ;
NODE * tmp = root;
if(tmp){
printf("%d(%d) ",tmp->data, tmp->high);
preorder_traversal(tmp->left);
preorder_traversal(tmp->right);
}
}
int hight(NODE * root)
{
if(!root)
return -1;
else
return root->high;
}
int max(int l, int r)
{
return (l > r ? l : r);
}
/*这四个旋转函数非NULL在调用之前就已经判断*/
NODE *single_rotate_left(NODE * root)//整个树是左旋的单旋转
{
NODE * tmp = root->left;
root->left = tmp->right;
tmp->right = root;
//旋转后深度随之变化,需要修改深度
root->high = max(hight(root->left), hight(root->right)) + 1;
tmp->high = max(hight(tmp->left), hight(tmp->right)) + 1;
return tmp;//返回新的子树根节点
}
NODE *single_rotate_right(NODE * root)//整个树是右旋的单旋转
{
NODE * tmp = root->right;
root->right = tmp->left;
tmp->left = root;
root->high = max(hight(root->left), hight(root->right)) + 1;
tmp->high = max(hight(tmp->left), hight(tmp->right)) + 1;
return tmp;
}
NODE *double_rotate_left(NODE * root)//整个树是左旋的双旋转
{
root->left = single_rotate_right(root->left);//先将右子树右旋并接收改变之后的右子树的根节点
return single_rotate_left(root);//再将整个树左旋
}
NODE *double_rotate_right(NODE * root)//整个树是右旋的双旋转
{
root->right = single_rotate_left(root->right);//先将左子树左旋并接收改变之后的左子树的根节点
return single_rotate_right(root);//再将整个树右旋
}
/*先根据二叉排序树规则插入,插完后进行平衡判断*/
NODE * insert_node(NODE * root, NODE * newnode)
{
if(!root)
return newnode;
NODE * tmp = root;
if(tmp->data > newnode->data){
tmp->left = insert_node(tmp->left, newnode);//递归向左子树插入
//旋转
if(hight(tmp->left) - hight(tmp->right) == 2){
if(tmp->left->data > newnode->data)//插到左子树左边,右旋(单旋转)
tmp = single_rotate_left(tmp);
else//插到左子树右边,左子树先左旋整个树再右旋(双旋转)
tmp = double_rotate_left(tmp);
}
}
else if(tmp->data < newnode->data){
tmp->right = insert_node(tmp->right, newnode);//递归向右子树中插入
//旋转
if(hight(tmp->right) - hight(tmp->left) == 2){
if(tmp->right->data < newnode->data)//插到右子树右边,左旋(单旋转)
tmp = single_rotate_right(tmp);
else//插到右子树左边,右子树先右旋整个树再左旋(双旋转)
tmp = double_rotate_right(tmp);
}
}
tmp->high = max(hight(tmp->left), hight(tmp->right)) + 1;
return tmp;
}
int main(int argc, char *argv[])
{
if(argc != 2){
printf("Parameter is failure!\n");
exit(EXIT_FAILURE);
}
int num = atoi(argv[1]);
srand(time(NULL));//用随机数做测试
NODE * root = NULL;
int i;
//由于程序中没有对相同节点处理,所以测试时可能会有实际插入树中的节点个数少于num的情况
for(i = 0; i<num; i++){
NODE * tmp = construct_node(rand()%1000);
root = insert_node(root, tmp);
}
/*打印出先序与中序遍历结果验证是否符合AVL树的规则创建*/
inorder_traversal(root);
printf("\n");
preorder_traversal(root);
printf("\n");
return 0;
}
我们简单运行测试一下,下图为每次插入后的中序与先序遍历结果(我在循环中打印了每插入一个节点后的中序遍历与先序遍历结果,可以观察每次的插入都满足AVL树的限制):
还原最终的树如图(其结果是满足AVL树特征的):
扩大到节点数为20(第一行为中序、第二行为先序),并还原二叉树后如下图: