AVL树

平衡二叉查找树(Self-balancing binary search tree)又被称为AVL树(AVL树是根据它的发明者G. M. Adelson-Velskii和E. M. Landis命名的),是在二叉查找树的基础上一个优化的版本

AVL树的特点:

1.本身首先是一棵二叉查找树
2.带有平衡条件:每个结点的左右子树的高度之差的绝对值不超过1,也就是说,AVL树,本质上是带了平衡功能的二叉查找树
如果读者关于二叉查找树还不了解可以看一下这篇随笔:二叉查找树(查找、插入、删除)

AVL树的作用

AVL树解决了二叉查找树可能出现的极端情况,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定,但是在某些极端情况下

(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,例如在在删除时,

我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,使得它的操作时间复杂度增加

例如下面这种情况:

AVL树的特性让二叉搜索树的节点实现平衡(balance):节点相对均匀分布,而不是偏向某一侧

AVL树节点的定义:

 typedef struct AVLTreeNode
 {
     int data;
     int height;    //节点的高度
     struct BSTreeNode *left;//左子树
     struct BSTreeNode *right;//右子树

 }AVLTree;

与一般的二叉查找树的节点相比多了一个参数,节点的高度(网上有些博客是把平衡因子加在了节点的定义里面,笔者不太建议这样做)

预备知识

为了读者能更好了理解AVL树的操作,在继续往下看之前需要搞清楚几个概念

高度深度平衡因子

(1)深度——从上往下数

节点的层次(节点的深度):从根开始定义,根为第1层,根的子节点为第2层,以此类推;(这里说根节点为第1层,其他博客可能把根节点定义成第0层,两种记法都没有错,都可以用来描述树的性质,只需要标注(>0)或者(>=0)做一个区分和解释即可),本篇随笔记根节点为第1层(也可以说成根节点的深度为1)
树的深度:树中节点的最大层次

(树的深度 = 叶子节点的深度)

(2)高度——从下往上数

关于高度,有的文章中将"空二叉树的高度定义为-1",本篇随笔采用维基百科上的定义:空二叉树的高度为0,因为高度是从下往上数,所以叶子节点的高度为1

(树的高度 = 根节点的高度)

(3)平衡因子

某结点的左子树与右子树的高度或深度(高度深度都可以,本篇随笔使用深度来计算平衡因子)差即为该结点的平衡因子(BF,Balance Factor),平衡二叉树(AVL树)上所有结点的平衡因子只可能是 -1,0 或 1
从上面的节点的定义可以看出,节点中存储的是节点的高度,而不是平衡因子
 
下图中就标注了所有节点的平衡因子

(平衡因子计算时左子树 - 右子树 和 右子树 - 左子树 都可以,因为判断树是否平衡的条件是:每个结点的左右子树的高度之差的绝对值不超过1,只不过判断失衡以后还要判断是哪一种失衡,这就需要根据情况来选择是左-右还是右-左了)

------------------------------------------------------------------------------------------------------------------------------------------------

1、查找节点

在 AVL树 中查找与在 二叉查找树 中查找完全一样,因为AVL树总是保持平衡的,树的结构不会由于查询而改变,这里就不再赘述了

实现代码:

 /* 查找特定值 */
 void SearchData(int targ, BSTree *nod)
 {
     if (nod != NULL)
     {
         if (nod->data == targ)
         {
             printf("查找值存在,值为%d\n", nod->data);
         }
         else if (nod->data > targ)
         {
             SearchData(targ, nod->left);    //递归查找左子树
         }
         else if (nod->data < targ)
         {
             SearchData(targ, nod->right);    //递归查找右子树
         }
     }
     else if (nod == NULL)
     {
         printf("查找值不存在\n");
     }
 }

2、插入节点(递归实现)

先梳理一下步骤

先来实现搜索最低失衡节点,搜索最低失衡节点是从新插入的节点(也就是叶子节点)往上搜索(也可以说成从新增结点开始向根部回溯),搜索到的第一个平衡因子>1(|左子树高度-右子树高度|>1)的节点,作为最低失衡节点,因为是从新插入的节点往上搜索,二叉树的搜索是单向的(结构体成员中只有左右子树),单独使用一个函数来实现逆向搜索实现起来并不方便,这里就把搜索最低失衡节点的操作放到递归实现的插入操作中

这里没有像上一篇随笔:二叉查找树(查找、插入、删除)——C语言那样先手动输入的一个二叉平衡树(因为这里要考虑节点的高度,输入不太方便),干脆就从空二叉树开始插入

实现代码:

/* 获取节点高度,空树的高度为0 */
int GetNodeHeight(AVLTree *nod)
{
    if (nod != NULL)    //若不为空子树
    {
        if (nod->left == NULL && nod->right == NULL)    //若为叶子节点
        {
            ;
        }
        else if (GetNodeHeight(nod->right) > GetNodeHeight(nod->left))    //若右子树高度较高
        {
            ;
        }
        else    //若左子树高度较高
        {
            ;
        }
    }
    else    //若为空子树
    {
        ;
    }
}

/* 添加新节点(包含搜索最低失衡节点和调整树操作) */
AVLTree *AddNewNode(AVLTree *nod, int NewData)
{
    AVLTree *p = NULL;

    if (nod == NULL)
    {
        if ((nod = (AVLTree *)malloc(sizeof(AVLTree))) == NULL)    //创建新节点
        {
            printf("内存不足");
            exit();
        }
        nod->data = NewData;
        nod->left = NULL;
        nod->right = NULL;
        nod->height = GetNodeHeight(nod);
    }
    else if (NewData > nod->data)
    {
        nod->right = AddNewNode(nod->right, NewData);
        nod->height = GetNodeHeight(nod);

        )    //右子树高度 - 左子树高度
        {

        }

        return nod;
    }
    else if (NewData < nod->data)
    {
        nod->left = AddNewNode(nod->left, NewData);
        nod->height = GetNodeHeight(nod);

        )    //左子树高度 - 右子树高度
        {

        }

        return nod;
    }
    else if (NewData == nod->data)
    {
        printf("不允许插入重复值");
        exit();
    } 

    return nod;
}

(若二叉树中只有根节点,那么这个根节点也是叶子节点)

在上面的代码中已经实现了插入新节点并且搜索最低失衡节点的功能,这里可以用前序遍历二叉树并打印节点高度来判断插入节点函数是否正确(上面预留的调整二叉树函数的位置)

遍历二叉树:

 /* 前序遍历AVL树,并打印节点高度 */
 void PreOrder_Traverse(AVLTree *nod)
 {
     if (nod != NULL)
     {
         printf("data = %d height = %d\n", nod->data, nod->height);

         PreOrder_Traverse(nod->left);
         PreOrder_Traverse(nod->right);
     }
 }

测试插入函数(保证每次插入新节点后的二叉树都是二叉平衡树)

测试数据图解:

测试结果:

搞清楚了各个节点的高度,平衡因子的计算也比较方便了,下面就是AVL树的核心操作“旋转”,不同的失衡情况有不同的旋转方式,一共有四种节点失衡情况,如下图

不同失衡情况下的示例二叉树,如下图(读者可能会发现“最低失衡节点的左子树的左子树还有非空节点”这个判断依据,对第二组图适用,但对于第一组图不太合适)

或者是

(LL型和RR型的操作相对简单)

第一种:LL型

LL型失衡,调整二叉树需要两步

第一步:将失衡节点的左子树的右子树 变成 失衡节点的左子树

第二步:失衡节点 变成 失衡节点未发生操作前左子树的右子树

只看上面的叙述有点绕,下面为实现代码和图片示例

实现代码:

 /* LL型旋转 */
 AVLTree * LL_Rotation(AVLTree *nod)
 {
     AVLTree *temp;
     temp = nod->left;    //临时保存nod的左子树

     nod->left = nod->left->right;    //将失衡节点的左子树的右子树 变成 失衡节点的左子树
     temp->right = nod;    //失衡节点 变成 temp的右子树

     nod->height = GetNodeHeight(nod);    //更新节点高度
     temp->height = GetNodeHeight(temp);    

     return temp;
 }

LL型旋转图解

GIF图:

LL型失衡测试:

测试数据:

测试结果:

第二种:RR型

RR型的操作和基本相同,只是方向相反,这里就不再赘述了

实现代码:

 /* RR型旋转 */
 AVLTree * RR_Rotation(AVLTree *nod)
 {
     AVLTree *temp;
     temp = nod->right;    //临时保存nod的右子树

     nod->right = nod->right->left;
     temp->left = nod;

     nod->height = GetNodeHeight(nod);    //更新节点高度
     temp->height = GetNodeHeight(temp);    

     return temp;
 }

第三种:LR型

LR型失衡的操作相比于LL型失衡操作相对要复杂一点,需要旋转两次才能恢复平衡

第一步:对失衡节点的左子树进行RR型旋转

第二步:对失衡节点进行LL型旋转

因为之前已经写好了LL型和RR型的旋转,这里直接用就可以了,实现代码如下

 /* LR型旋转 */
 AVLTree * LR_Rotation(AVLTree *nod)
 {
     nod->left = RR_Rotation(nod->left);
     nod = LL_Rotation(nod);

     return nod;
 }

LR型旋转图解:

测试数据:

第三种:RL型

和LR型的旋转基本相同,这里就不再赘述了,实现代码如下

 /* RL型旋转 */
 AVLTree * RL_Rotation(AVLTree *nod)
 {
     nod->right = LL_Rotation(nod->right);
     nod = RR_Rotation(nod);

     return nod;
 }

3、删除节点

删除节点比插入节点的操作还要稍微复杂一点,因为插入时,进行一次平衡处理(一次平衡处理可能包含多次旋转),整棵树都会处于平衡状态,而在删除时,需要进行多次平衡处理,才能保证树处于平衡状态

AVL树的删除操作前半部分和二叉查找树相同,只不过删除后要检查树是否失去平衡,如果失衡就需要重新调整平衡,并更新节点高度,总的来说可以分为如下几种情况

(1)删除叶子节点

情况一:删除节点后二叉树没有失去平衡

删除节点后树没有失去平衡,这种情况下只需要更新节点的高度

情况二:删除节点后二叉树失去平衡

上图的RE型失衡只有在删除操作时才可能出现(在插入时不可能出现),RE型失衡的旋转方式和RR型失衡的旋转方式一模一样

(虽然删除节点时遇到的失衡情况多了两种 LE和RE ,但是旋转的方式依旧是那四种(LL、RR、LR、RL))

实现代码:

 /* 删除节点 */
 AVLTree *DeletNode(AVLTree *nod, int DelData)
 {
     AVLTree *SNode = NULL; //后继节点
     AVLTree *PSNode = NULL;    //后继节点的父节点
     AVLTree *temp = NULL;    //临时保存待释放节点的子树,避免free后找不到左右子树

     if (nod == NULL)
     {
         printf("删除节点不存在");
         exit();
     }
     else if (DelData > nod->data)
     {
         nod->right = DeletNode(nod->right, DelData);

         )
         {
             temp = nod->left;

             if (GetNodeHeight(temp->left) >= GetNodeHeight(temp->right))    //LL型或LE型失衡、两种情况处理方式相同
             {
                 nod = LL_Rotation(nod);
             }
             else    //LR型失衡
             {
                 nod = LR_Rotation(nod);
             }
         }

         nod->height = GetNodeHeight(nod);    //更新节点高度
     }
     else if (DelData < nod->data)
     {
         nod->left = DeletNode(nod->left, DelData);

         )
         {
             temp = nod->right;

             if (GetNodeHeight(temp->right) >= GetNodeHeight(temp->left))    //RR或RE型失衡、两种情况处理方式相同
             {
                 nod = RR_Rotation(nod);
             }
             else    //RL型失衡
             {
                 nod = RL_Rotation(nod);
             }
         }

         nod->height = GetNodeHeight(nod);    //更新节点高度
     }
     else if (DelData == nod->data)
     {
         if (nod->right == NULL && nod->left == NULL)    //若待删除节点为叶子节点
         {
             free(nod);
             return NULL;
         }
     }

(2)删除带有一个子节点的节点

 else if (DelData == nod->data)
     {
         if (nod->right == NULL && nod->left == NULL)    //若待删除节点为叶子节点
         {
             free(nod);
             return NULL;
         }
         else if (nod->right == NULL && nod->left != NULL)    //若待删除节点只有左子树
         {
             temp = nod->left;
             free(nod);

             return temp;
         }
         else if (nod->right != NULL && nod->left == NULL)    //若待删除节点只有右子树
         {
             temp = nod->right;
             free(nod);

             return temp;
         }
     }

(3)删除带有两个子节点的节点

删除带有两个子节点的节点时,需要找到待删除的节点的后继节点或者前驱节点(本篇随笔使用后继节点),具体方法在上一篇随笔已经列出二叉查找树(查找、插入、删除)——C语言,这里不再赘述

 else    //若待删除节点既有左子树也有右子树
         {
             SNode = SearchSuccessorNode(nod->right);    //搜索后继节点
             PSNode = SearchParentofSNode(nod->right, nod->right);    //搜索后继节点的父节点

             if (nod->right == SNode)    //后继节点为待删除节点的右子树(后继节点有右子树和没有右子树的操作相同)
             {
                 SNode->left = nod->left;
                 free(nod);

                 return SNode;
             }
             else if (nod->right != SNode && SNode->right == NULL)    //后继节点不为待删除节点的右子树,并且该后继节点没有右子树
             {
                 SNode->left = nod->left;
                 SNode->right = nod->right;
                 PSNode->left = NULL;
                 free(nod);

                 return SNode;
             }
             else if (nod->right != SNode && SNode->right != NULL)    //后继节点不为待删除节点的右子树,并且该后继节点有右子树
             {

                 PSNode->left = SNode->right;    //后继节点的右子树作为后继节点父节点的左子树
                 SNode->left = nod->left;
                 SNode->right = nod->right;
                 free(nod);

                 return SNode;
             }
         }
     }

需要注意的是,删除节点时不会出现“后继节点不是删除节点的子节点,且后继节点有右子树”这种情况,如下图

上图的14节点已经失衡了,在插入的时候就会被调整,所以不会出现“后继节点不是删除节点的子节点,且后继节点有右子树”

(由于笔者能力有限,AVL树的删除操作分析的不是很清楚,若有疏漏,清指出)

AVL树(查找、插入、删除)——C语言的更多相关文章

  1. AVL树的插入删除查找算法实现和分析-1

    至于什么是AVL树和AVL树的一些概念问题在这里就不多说了,下面是我写的代码,里面的注释非常详细地说明了实现的思想和方法. 因为在操作时真正需要的是子树高度的差,所以这里采用-1,0,1来表示左子树和 ...

  2. AVL 树的插入、删除、旋转归纳

    参考链接: http://blog.csdn.net/gabriel1026/article/details/6311339   1126号注:先前有一个概念搞混了: 节点的深度 Depth 是指从根 ...

  3. AVL树的插入操作(旋转)图解

    =================================================================== AVL树的概念       在说AVL树的概念之前,我们需要清楚 ...

  4. AVL树的插入与删除

    AVL 树要在插入和删除结点后保持平衡,旋转操作必不可少.关键是理解什么时候应该左旋.右旋和双旋.在Youtube上看到一位老师的视频对这个概念讲解得非常清楚,再结合算法书和网络的博文,记录如下. 1 ...

  5. Avl树的基本操作(c语言实现)

    #include<stdio.h> #include<stdlib.h> typedef struct AvlNode *Position; typedef struct Av ...

  6. 第七章&#160;二叉搜索树 (d2)AVL树:插入

  7. AVL树(平衡二叉查找树)

    首先要说AVL树,我们就必须先说二叉查找树,先介绍二叉查找树的一些特性,然后我们再来说平衡树的一些特性,结合这些特性,然后来介绍AVL树. 一.二叉查找树 1.二叉树查找树的相关特征定义 二叉树查找树 ...

  8. 数据结构--Avl树的创建,插入的递归版本和非递归版本,删除等操作

    AVL树本质上还是一棵二叉搜索树,它的特点是: 1.本身首先是一棵二叉搜索树.   2.带有平衡条件:每个结点的左右子树的高度之差的绝对值最多为1(空树的高度为-1).   也就是说,AVL树,本质上 ...

  9. AVL树原理及实现(C语言实现以及Java语言实现)

    欢迎探讨,如有错误敬请指正 如需转载,请注明出处http://www.cnblogs.com/nullzx/ 1. AVL定义 AVL树是一种改进版的搜索二叉树.对于一般的搜索二叉树而言,如果数据恰好 ...

  10. 深入浅出数据结构C语言版(12)——平衡二叉查找树之AVL树

    在上一篇博文中我们提到了,如果对普通二叉查找树进行随机的插入.删除,很可能导致树的严重不平衡 所以这一次,我们就来介绍一种最老的.可以实现左右子树"平衡效果"的树(或者说算法),即 ...

随机推荐

  1. ASP.NET MVC: Razor中的@:和语法

    本文将讨论新版Razor里视图引擎中支持的两个有用的语法功能:@:和<text>语法. 用Razor实现流畅编程 ASP.NET MVC 3配有一个新的名为“Razor”的视图引擎选项(除 ...

  2. JSP开发模式2_JSP/Servlet/JavaBean(简单注册功能)

    import java.util.regex.Matcher;import java.util.regex.Pattern; public class RegisterBean {    privat ...

  3. 学习varnish随笔

    Varnish是一款高性能.开源的反向代理服务器和缓存服务器.Varnish使用内存缓存文件来减少响应时间和网络带宽消耗.这个项目是由挪威的一家报纸Verdens Gang的网络分支起始的,其架构设计 ...

  4. Xcode文档安装

    找到所需文档的下载地址,搜索.dmg 安装位置

  5. HTML的定位属性

    position    用于定义一个元素是否absolute(绝对),relative(相对),static(静态),或者fixed(固定) top层距离顶点纵坐标的距离 left层距离顶点横坐标的距 ...

  6. oracle 10g

    一.安装系统 首先安装Linux系统,根据Oracle官方文档的建议,在机器内存小于1G的情况下,swap分区大小应该设置为内存的2倍大,若内存大于2G则swap分区设置为与内存大小一样. 为防止Or ...

  7. MySQL5.7 linux二进制安装

    200 ? "200px" : this.width)!important;} --> 介绍 MySQL5.7出来也有大半年了,业内也一直在宣传5.7有多么的N,官网的也是宣 ...

  8. 关于Failed to convert property value of type [org.quartz.impl.StdScheduler] to required type [org.springframework.scheduling.quartz.SchedulerFactoryBean

    在一个业务类有下列属性 private SchedulerFactoryBeanscheduler; public SchedulerFactory BeangetScheduler() { retu ...

  9. django form表单验证

    一. django form表单验证引入 有时时候我们需要使用get,post,put等方式在前台HTML页面提交一些数据到后台处理例 ; <!DOCTYPE html> <html ...

  10. 开源Spring解决方案--lm.solution

    Github 项目地址: https://github.com/liumeng0403/lm.solution 一.说明 1.本项目未按java项目传统命名方式命名项目名,包名 如:org.xxxx. ...