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

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树中插入一个节点时,可能会打破该树的平衡性,所以就需要重启对其进行平衡操作。

举个简单的例子:

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

在未进行插入操作以前,对于节点45来说,其左子树高度为2,右子树为1,平衡因子绝对值等于1(之后的均用左右子树之差的绝对值表示平衡因子,不需再强调),所以此时是平衡的。但是对于第一种插入情况:插入的值小于根节点,因此插入了高度本就大的左子树中,平衡因子绝对值被增大到2因此不再是平衡二叉排序树。

2、引起AVL树的失衡的四种情况:

既然AVL树的平衡会被打破,那么我们需要考虑的就是恢复AVL树的平衡,关于平衡的恢复有四种情况,分为单旋转两种和双旋转两种。所谓单旋转就是只需要旋转一次,而双旋转则是需要旋转两次。为什么会有单双之分?旋转的过程又是怎样的?首先来看会引起失衡的四种情况(每种情况的标题是之后会用到的解决方案):
(1)、单向右旋平衡处理(RR):
插入节点导致左边过重,向“右”平衡(旋转)。如图,插入新节点后失重(丢失平衡):

《AVL树的插入与旋转算法解析》
(插入10(小于18)与插入20(大于18但小于32)属于同一种情况RR,之后我们会以插入10为例分析旋转算法)

(2)、单向左旋平衡处理(LL):
《AVL树的插入与旋转算法解析》
同样,60(左)和80(右)对于旋转来说也是一样的。

(3)、双向旋转平衡处理(LR先左后右):
《AVL树的插入与旋转算法解析》
根节点的左子树的右子树也有两种情况,但都是一种解决方法,我们以右插为例。
(4)、双向旋转平衡处理(RL先右后左):
《AVL树的插入与旋转算法解析》
根节点的右子树的左子树也有两种情况,我们以左插为例。

3、解决失衡的妙招——旋转:

既然已经了解了会引起失衡的原因以及相关情况,我们就可以根据这四种情况来分析相关的旋转算法了(由于单旋转RR与LL思想是相似的,而双旋转RL和LR的思想也是相似的,所以我们分别以RR和LR做重点分析):
(1)、单旋转RR:

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

(2)、双旋转LR:
为什么向左子树的左子树中插入是单旋转,而往左子树的右子树中插入是双旋转呢?其实很简单:一次旋转之后达不到平衡(因为对LR的情况来说,新节点插入在右子树时,若还采用原有的单旋转会将本来的一边失重情况转到另一边失重,达不到目的),所以想到去再旋转一次(就像求表达式极限时,用一次洛必达法则求不出,就会想到对第一次的结果再用一次洛必达法则而去求二阶导)。但是这里的二次旋转并不是对同一个根节点旋转两次(右旋转第一次平衡不了,对根节点再左旋转一次会恢复到最初的不平衡状态,大家可以自己试一试就知道了),那么我们看看LR的实际旋转过程:

《AVL树的插入与旋转算法解析》
而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树的插入与旋转算法解析》

还原最终的树如图(其结果是满足AVL树特征的):
《AVL树的插入与旋转算法解析》

扩大到节点数为20(第一行为中序、第二行为先序),并还原二叉树后如下图:
《AVL树的插入与旋转算法解析》

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

    原文作者:AVL树
    原文地址: https://blog.csdn.net/apollon_krj/article/details/69258609
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞