AVL树的插入与旋转算法解析

来源:互联网 发布:长安张宝林怎么样知乎 编辑:程序博客网 时间:2024/05/16 19:13

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(第一行为中序、第二行为先序),并还原二叉树后如下图:
这里写图片描述

这里写图片描述

0 0