数据结构(十五) -- C语言版 -- 树 - 二叉树的操作进阶之创建、插入、删除、查询、销毁

零、读前说明

  • 本文中所有设计的代码均通过测试,并且在功能性方面均实现应有的功能。
  • 设计的代码并非全部公开,部分无关紧要代码并没有贴出来。
  • 如果你也对此感兴趣、也想测试源码的话,可以私聊我,非常欢迎一起探讨学习。
  • 由于时间、水平、精力有限,文中难免会出现不准确、甚至错误的地方,也很欢迎大佬看见的话批评指正。
  • 嘻嘻。。。。 。。。。。。。。收!

一、概述

  前段时间因为工作关系,用到了二叉树的相关知识,起初使用前面总结的二叉树的代码美滋滋的编写着代码(心里OS:什么玩意儿,小case!),直到在调用测试案例的才发现,这尼玛,我还不知道遍历顺序是什么呢,而我现在需要的是创建一个空树,然后将我现在知道的所有节点链接到树中,但是还有一些暂时不知道但是后续在运行过程中需要插入的节点,明显咱们之前的那些创建方式中没有说明过树的节点插入的这回事,那该怎么办呢?
  俗话说的话,老天饿不死瞎家鹊,动起小脑瓜,找!(因为不想造轮子,这方面的知识已经被各种大佬弄得就像透明的玻璃纸样,到处可见)。还好找到了比较契合我需要的方式,那么下面就介绍这种沁人心脾的操作吧!!!

  嘻嘻嘻 (#^ . ^#) …

二、数据模型分析创建

2.1、节点的结构模型

  前面我们已经介绍过一种通用链表的创建与操作的文章了,详细情况可以转到原先的链表部分的去详细查看。

  数据结构(三) – C语言版 – 线性表的链式存储 - 单链表。。。。

  我们是不是也可以通过用链表的这种形式去创建和操作树呢,答案当然是肯定的!
  那么根据通用链表的形式,我们也可以定义出来二叉树的结构体模型。

/**
 * 数据类型封装 
 **/
typedef void BiTree;

  那么对于指针域来说,与单链表相比较,此处二叉树唯一的不同就是在指针域中有两个指针而已,也就是每个节点中,有两个指针而已。一个 lchild, 一个 rchild

/****
 * 节点的指针域
 * 单链表中一个指针,树中两个指针
 ****/
typedef struct _tag_BiTreeNode
{
    struct _tag_BiTreeNode *lchild;
    struct _tag_BiTreeNode *rchild;
} BiTreeNode;

  那么二叉树的节点来说,我们就可以这样定义了,里面除了包含二叉树的指针域之外,再加入一个二叉树节点统计的变量,类似于链表中的 length 域。

/****
 * 二叉树节点的结构的定义
 ****/
typedef struct _tag_BiTree
{
    BiTreeNode *root; /* 指向二叉树的根节点 */
    int count;        /* 二叉树中有多少个节点 */
} TBiTree;

  在一个树中,我们肯定、必须需要知道其根节点位置,那么根据根节点,我们可以通过一定的路径和分叉就能定位到一个需要查询的节点,举个栗子先。。。。
  
在这里插入图片描述

图2.1 举个栗子

  

  在上面的这个树中,如果我们想要找到B节点,那么只需要从根节点出发往左边分支走一步即可到达。如果我们想要找到E节点,那么也是从根节点出发先往左边走一步到达B节点,然后再往右边分支走一步即口到达心心念念的E节点

  从上面的分析可以看出来,我们需要定位节点的时候,需要明确的知道是往左边分支走?还是往右边分支走?还有需要走几步?如此即可确定位置。所以,我们可以确定上面二叉树中各个节点的位置信息了,就是下表中这样。

表2.1 节点的位置表示表
编号 节点名称 相对于根节点的位置 移动步数
01 A - 0
02 B 左边 1
03 C 右边 1
04 D 左边、左边 2
05 E 左边、右边 2

  所以,在上面的结构的基础上,我们在定位节点的过程中还需要知道位置信息,以及左分支还是右分支的选择性问题。那么我们可以定义这样的事情来规定谁左谁右。

/****
 * 定义是左分支还是右分支的标识
 ****/
#define BiT_BRACH_L 0
#define BiT_BRACH_R 1

/**
 * 目标节点相对于根节点的位置信息 
 **/
typedef unsigned long long BiTNodePos;

表2.2 节点的位置表示表
编号 节点名称 相对于根节点的位置 BiTNodePos 移动步数
01 A - 0x00 0
02 B 左边 0x00 1
03 C 右边 0x01 1
04 D 左边、左边 0x02 2
05 E 左边、右边 0x02 2

2.2、操作函数结构模型

  为了在调用、修改的时候方便,使用结构体进行操作函数的定义与申明,具体使用的结构体定义如下。

typedef void(BiTree_Printf)(BiTreeNode *);

typedef struct __tag_func_BiTree
{
    BiTree *(*create)();
    int (*insert)(BiTree *, BiTreeNode *, BiTNodePos, int);
    BiTreeNode *(*delete)(BiTree *, BiTNodePos, int);
    BiTreeNode *(*get)(BiTree *, BiTNodePos, int);
    int (*update)(BiTree *, BiTNodePos, int, BiTreeNode *);
    void (*display)(BiTree *, BiTree_Printf *, int);
    void (*clear)(BiTree *);
    int (*depth)(BiTree *);
    int (*count)(BiTree *);
    int (*degree)(BiTree *);
    BiTreeNode *(*reset)(BiTree *);
    void (*destroy)(BiTree *);
} TFBiTree;

三、创建

  基于上面的分析,那么在此种结构模型下创建一个二叉树就像创建一个链表,短短的几行代码即可完成创建。

/**
 * 功 能:
 *      创建一个二叉树
 * 参 数:
 *      无
 * 返回值:
 *      成功:创建树的根节点
 *      失败:NULL
 **/
BiTree *BiTree_Create(void)
{
    TBiTree *ret = NULL;

    ret = (TBiTree *)malloc(sizeof(TBiTree));

    if (ret != NULL)
        goto error;

    memset(ret, 0, sizeof(TBiTree));

error:
    return ret;
}

so easy!

四、插入

  在概述部分已经详细说明了思路,下面从三个方面来说明插入的过程。

4.1、在树中的某一个叶子节点位置插入新节点

  假设有这样一个二叉树(下图左),现在往树中插入一个新的节点 F,作为节点 E 的右子树,那么:

    首先:需要确定将 F 节点插入到什么位置 ------ E 的右子树位置
    然后:需要明确应该如何定位到要插入的位置 ----- E 的右子树
    最后:将 Erchild 指针域赋值就可以了

  根据上面的描述,那么插入节点 F 后的树应该是这样的(下图右)。
  
在这里插入图片描述

图4.1 插入节点效果图


  首先从根节点开始(黑色的 current 指针),依次经过左转到达节点 B 绿色的 current 指针 ),然后右转到达节点 E 蓝色的 current 指针 ),然后在右转即可定位到要插入的节点的位置 红色的 current 指针 )。

   此时可知:相对于根节点而言,要插入节点的位置信息为:左右右
  
在这里插入图片描述

图4.2 节点定位示意效果图

  但是还存在一个问题,就是经过这么一折腾,current 指针指向一个空指针(NULL)了,原则上来说已经不能进行操作了。所以此时我们还需要用一个指针来记录 current 指针在走进死胡同(指向NULL)之前最后的一次经过的有效记录节点(图中的E节点),此时假设定义为 parent 指针,那么在 current 指针变化的过程中,parent 指针变化的情况为下图这样。

在这里插入图片描述

图4.3 current指针和parent指针的一定关系表示

  
  parent 初始为指向NULL,然后在 current 指针移动的时候,记录 current 指针的上次的状态,图中对应的颜色为 current 指针和 parent 指针相对应的情况。
  此时,判断可知 current 指针指向的位置为新插入节点的位置,那么 parent 节点即为要插入节点的父亲节点,所以应该

parent->rchild = F;

  
  这样的话,新节点 F 已经成功的插入法到树中了。

  但是这只是一种情况,即将新节点插入到树中的叶子节点的后面。
  但是两种情况呢应该是怎么样的呢

    1、空树中插入新节点
    2、在树中的某一个非叶子节点位置插入新节点

  下面进行一一说明。

4.2、在树中的某一个非叶子节点位置插入新节点

  同样的,比如将节点 F 插入到 B 节点之后,并且成为 B左子树,那么此时, current 指针和 parent 指针的分布应该是下图这样的。

在这里插入图片描述

图4.4 节点插入示意效果图1

  
  图中可以明显看出来,如果将 F 插入到该插入的位置,那么此时 B 节点的子树成了三个了,此时严重的与二叉树不符,那应该怎么办呢?作为一个把一块钱当成十块钱来花的我来说,既然将新节点 F 插入成为 B 节点的左子树,那么 B 节点的原来的左子树咱们也不能丢弃,将 B 节点原来的左子树直接送给新插入的 F 节点作为其左子树
  就像下图这样,一毛不拔!!!全部自己消化,哈哈哈。。。。

在这里插入图片描述

图4.5 节点插入示意效果图2

  
  从上面分析可以明显看出来,想弄到哪边就是哪边,但是应该如何去判断到底是哪边呢?那这里我们约定一下了,不能由着性子来了,毕竟是老男孩了,稳重点为好。

  我们先看看新插入的节点替换的是 parent 指针指向的节点的左子树还是右子树
    如果替换的是左子树,那么就让此时的 current 指针指向的节点成为新插入节点的左子树
    如果替换的是右子树,那么就让此时的 current 指针指向的节点成为新插入节点的右子树

  这样就可以完成原来树中此节点后面的子树的链接操作,并且保留原来节点的左右属性。

4.3、在空树中插入新节点

  呵!空树!插入节点!哈!那不就是要插入根节点嘛!!!弄得这么正式。。。
  
在这里插入图片描述

图4.6 节点插入示意效果图3

  
  哈哈,动都不带动的就搞定。

4.4、代码编写

  综上所述,代码似乎已经跃然纸上,那么下面就直接贴上代码,注释比较详细,此处不再赘述。

/**
 * 功 能:
 *      插入节点
 * 参 数:
 *      tree :要操作的树
 *      node :要插入的节点
 *      pos  :相对于根节点的位置(二进制表示)
 *      steps:移动的步数
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int BiTree_Insert(BiTree *tree, BiTreeNode *node, BiTNodePos pos, int steps) // O(n)
{
    TBiTree *BiTree = (TBiTree *)tree;
    int ret = 0, bit = 0;

    /* 判断参数异常 */
    if (BiTree == NULL || node == NULL || pos < 0 || steps < 0)
    {
        ret = -1;
        goto ERROR;
    }
    /* 临时指针,用于记录current移动的记录,初始化为NULL */
    BiTreeNode *parent = NULL;
    /* 从根节点出发,初始化为根节点,是要存入节点的位置 */
    BiTreeNode *current = BiTree->root;

    /* 对新加入的节点进行初始化,左子树和右子树为空 */
    node->lchild = NULL;
    node->rchild = NULL;

    /****
     * 定位节点的操作
     ****/
    /* 判断要拐的次数,并且current不能为NULL */
    while ((steps > 0) && (current != NULL))
    {
        /* 取pos的最低位与1进行与操作,可以确定是往左走还是往右走 */
        bit = pos & 1;
        /* pos右移 */
        pos = pos >> 1;
        /* 记录当前指针 */
        parent = current;
        /***
         *  由于在树中,有两个指针域,所以需要通过标志位来判断是往左走还是往右走 
         *  也是一条路走到底,类似于递归
         *  将线性的方法引进到树中,类似于链表的后移
         ***/
        if (bit == BiT_BRACH_L)
        {
            /* 向左转,类似于链表后移的操作 */
            current = current->lchild;
        }
        else if (bit == BiT_BRACH_R)
        {
            /* 向右转,同样类似于链表后移的操作 */
            current = current->rchild;
        }
        /* 操作次数进行--操作 */
        steps--;
    }

    /* 判断parent指针是否为空 */
    if (parent != NULL)
    {
        /**
         *  判断最后一次是从那边走过来的 
         *  如果是从左边走过来的:node节点指向partent的左子树
         **/
        if (bit == BiT_BRACH_L)
        {
            parent->lchild = node;
            /*** 
             * 当前位置是插入新节点的位置,
             * 如果是左边的子树,将当前节点的位置赋值给新插入节点的左子树 
             * 完成原来树中此节点后面的子树的链接操作
             ***/
            node->lchild = current;
        }
        else if (bit == BiT_BRACH_R)
        {
            /**
             *  判断最后一次是从那边走过来的
             *  如果是从右边走过来的:node节点指向partent的右子树
             **/
            parent->rchild = node;
            /*** 
             * 当前位置是插入新节点的位置,
             * 如果是右边的子树,将当前节点的位置赋值给新插入节点的右子树 
             * 完成原来树中此节点后面的子树的链接操作
             ***/
            node->rchild = current;
        }
    }
    else
    {
        /* 如果到这儿了,那么说明是插入的是根节点,那么就是将node赋值给root */
        BiTree->root = node;
    }
    /* 插入新节点,count进行自加操作 */
    BiTree->count++;

ERROR:
    return ret;
}

五、删除

  上面详细说明了插入一个新节点的过程以及操作,那么对于想要删除一个节点的操作,在90%的地方的操作是一致的,只是在找到要删除的节点的时候,将插入的操作换成删除的操作即可。那么问题同样还是:

    1、如何找到要删除的节点
    2、具体删除的操作操作,细节应该怎么处理

  好,有问题才是最常规的情况,解决问题才是我们存在的理由,那么下面咱们开始。

5.1、删除叶子节点

在这里插入图片描述

图5.1 删除叶子节点示意图


  举个栗子,如果我们需要删除节点F,那么:
  首先从根节点开始(黑色的 current 指针),依次经过左转到达节点 B 绿色的 current 指针 ),然后右转到达节点 E 蓝色的 current 指针 ),然后在右转即可定位到要删除的节点的位置 红色的 current 指针 )。

  此时可知:相对于根节点而言,要删除节点的位置信息为:左右右
  此时parent指针即为要删除节点的双亲结点,所以直接:

parent->rchild = NULL;

  就行了,没有任何难度而言。

5.2、删除非叶子节点


在这里插入图片描述

图5.2 删除非叶子节点示意图


  举个栗子,如果我们需要删除节点B,那么:
  首先从根节点开始(黑色的 current 指针),经过左转接口到达节点 B 红色的 current 指针 )。

  此时可知:相对于根节点而言,要删除节点的位置信息为:左

  此时 parent 指针即为要删除节点的双亲结点,但是又有新的问题出现了,我们删掉了节点 B ,那么 B 左子树或者右子树应该怎么安排呢,总不能不闻不问吧!
  是的,至于节点的安排的问题,具体还要看的你们需求或者目的了,我们整理列举几种可能的情况:

    1、删掉 B 节点,同时不管 B 是否存在左子树或者右子树,通通丢弃
    2、将节点 B 左子树或者右子树****继承给原来节点 B 双亲结点(上图中节点 A ),那么怎么将节点继承过去也是个不容忽视的问题:
      1)如果 B 只存在左子树不存在右子树(图中 D 节点),那么如果 B 左子树的话,正好可以补缺( D 替换原来 B 的位置),如果换成只有右子树也同样适用。
      2)如果删除的节点既存在左子树也存在右子树(图中节点 D ,节点 E ),那么怎么将这个左子树和右子树继承给节点 A 呢?是不是在没有具体的需求的前提下怎么搞都不太妙!

  本文章中直接采用第一种方式,删除某个节点,那么也一起删掉这个节点可能存在的左子树或者右子树,而且这种处理方式也不叫契合树的这个递归的思想,类似于删除一个文件夹,那么同步也删除这个文件夹中的子文件夹和所有文件这样。。。。。

  另外, 根据我们定义二叉树的结构,在删除节点的是需要将目前树中的节点个数减去删除节点的个数,在我们上面的分析我们删除一个节点的同事,同时删除了此节点可能存在的左子树或者右子树,那么节点的个数也减少至少1个,所以我们还需要计算出来删除节点的子树的节点个数。

5.3、代码编写

  所以综上所述,代码可以这样编写了。

/**
 * 功 能:
 *      删除树中指定的节点
 * 参 数:
 *      tree :要操作的树
 *      pos  :相对于根节点的位置(二进制表示)
 *      steps:移动的步数
 * 返回值:
 *      成功:删除后的树的节点
 *      失败:NULL
 **/
BiTreeNode *BiTree_Delete(BiTree *tree, BiTNodePos pos, int steps) // O(n)
{
    BiTreeNode *current = NULL;

    if (tree == NULL || pos < 0 || steps < 0)
        goto ERROR;

    TBiTree *BiTree = (TBiTree *)tree;
    current = BiTree->root;
    if (current == NULL)
        goto ERROR;

    BiTreeNode *parent = NULL;
    int bit = 0;

    /* 判断要拐的次数,并且current不能为NULL */
    while ((steps > 0) && (current != NULL))
    {
        /* 取pos的最低位与1进行与操作,可以确定是往左走还是往右走 */
        bit = pos & 1;
        /* pos右移 */
        pos = pos >> 1;
        /* 记录当前指针 */
        parent = current;
        /***
         *  由于在树中,有两个指针域,所以需要通过标志位来判断是往左走还是往右走 
         *  也是一条路走到底,类似于递归
         *  将线性的方法引进到树中,类似于链表的后移
         ***/
        if (bit == BiT_BRACH_L)
        {
            /* 向左转,类似于链表后移的操作 */
            current = current->lchild;
        }
        else if (bit == BiT_BRACH_R)
        {
            /* 向右转,同样类似于链表后移的操作 */
            current = current->rchild;
        }
        /* 操作次数进行--操作 */
        steps--;
    }

    if (parent != NULL)
    {
        /* 最后一次操作是左转,那么将lchild指针置为空 */
        if (bit == BiT_BRACH_L)
            parent->lchild = NULL;
        /* 最后一次操作是右转,那么将rchild指针置为空 */
        else if (bit == BiT_BRACH_R)
            parent->rchild = NULL;
    }
    /* 如果parent为空,则说明要不就是一个节点,要不就是删除根节点,反正结果都是下面这样 */
    else
        BiTree->root = NULL;

    /* 将树的节点个数减去相对应的个数 */
    BiTree->count = BiTree->count - BiTree_NodesCnt(current);

ERROR:
    return current;
}

六、查询

  上面详细说明了插入、删除一个新节点的过程以及操作,那么对于想要查询一个节点,将变得非常简单,我们只需要找到这个节点然后返回即可。代码就可以这样简答的搞定了。

/**
 * 功 能:
 *      获取树中指定的节点
 * 参 数:
 *      tree :要操作的树
 *      pos  :相对于根节点的位置(二进制表示)
 *      steps:移动的步数
 * 返回值:
 *      成功:获取后的树的节点
 *      失败:NULL
 **/
BiTreeNode *BiTree_Get(BiTree *tree, BiTNodePos pos, int steps) // O(n)
{
    TBiTree *BiTree = (TBiTree *)tree;
    BiTreeNode *current = NULL; //BiTree->root;
    if (BiTree == NULL || pos < 0 || steps < 0)
        goto ERROR;

    int bit = 0;

    current = BiTree->root;
    if (current == NULL)
        goto ERROR;

    /* 判断要拐的次数,并且current不能为NULL */
    while ((steps > 0) && (current != NULL))
    {
        /* 取pos的最低位与1进行与操作,可以确定是往左走还是往右走 */
        bit = pos & 1;
        /* pos右移 */
        pos = pos >> 1;
        /***
         *  由于在树中,有两个指针域,所以需要通过标志位来判断是往左走还是往右走 
         *  也是一条路走到底,类似于递归
         *  将线性的方法引进到树中,类似于链表的后移
         ***/
        if (bit == BiT_BRACH_L)
        {
            /* 向左转,类似于链表后移的操作 */
            current = current->lchild;
        }
        else if (bit == BiT_BRACH_R)
        {
            /* 向右转,同样类似于链表后移的操作 */
            current = current->rchild;
        }
        /* 操作次数进行--操作 */
        steps--;
    }

ERROR:
    return current;
}

七、修改

  上面详细说明了插入、删除一个新节点的过程以及操作,那么对于想要更新一个节点,同样非常简单,我们只需要找到这个节点,然后将目标值重新赋值就可以了。
  既然是要更新节点,就本工程中使用的模式来讲,我们其实是替换原来节点的双亲节点的左节点或者右节点,然后将原来节点的左子树和右子树替换到新节点的左子树和右子树上
  所以,代码就可以这样简单的搞定了。

/**
 * 功 能:
 *      更新树中指定的节点
 * 参 数:
 *      tree   :要操作的树
 *      pos    :相对于根节点的位置(二进制表示)
 *      steps  :移动的步数
 *      destVal: 要修改成的目标值, destVal 不得为 NULL
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int BiTree_Update(BiTree *tree, BiTNodePos pos, int steps, BiTreeNode *destVal) // O(n)
{
    BiTreeNode *current = NULL;
    int ret = -1;
    if (tree == NULL || destVal == NULL || pos < 0 || steps < 0)
        goto ERROR;

    TBiTree *BiTree = (TBiTree *)tree;
    current = BiTree->root;
    if (current == NULL)
        goto ERROR;

    int bit = 0;

    BiTreeNode *parent = NULL;
    /* 判断要拐的次数,并且current不能为NULL */
    while ((steps > 0) && (current != NULL))
    {
        /* 取pos的最低位与1进行与操作,可以确定是往左走还是往右走 */
        bit = pos & 1;
        /* pos右移 */
        pos = pos >> 1;
        /* 记录当前指针 */
        parent = current;
        /***
         *  由于在树中,有两个指针域,所以需要通过标志位来判断是往左走还是往右走 
         *  也是一条路走到底,类似于递归
         *  将线性的方法引进到树中,类似于链表的后移
         ***/
        if (bit == BiT_BRACH_L)
        {
            /* 向左转,类似于链表后移的操作 */
            current = current->lchild;
        }
        else if (bit == BiT_BRACH_R)
        {
            /* 向右转,同样类似于链表后移的操作 */
            current = current->rchild;
        }
        /* 操作次数进行--操作 */
        steps--;
    }
    /* 如果current为空,则说明要替换的节点不存在 */
    if (current == NULL)
    {
        goto ERROR;
    }

    /* 判断parent指针是否为空,类似于插入的操作 */
    if (parent != NULL)
    {
        if (bit == BiT_BRACH_L)
        {
            parent->lchild = destVal;
        }
        else if (bit == BiT_BRACH_R)
        {
            parent->rchild = destVal;
        }
    }
    else
    {
        /* 如果到这儿了,那么说明是根节点 */
        BiTree->root = destVal;
    }
    /* 将当前找到的节点的值修改成目标值,将旧节点的子树赋值给目标节点的子树 */
    destVal->lchild = current->lchild;
    destVal->rchild = current->rchild;
    /* 修改ret值 */
    ret = 0;

ERROR:
    return ret;
}

八、清空、销毁

  销毁一个树,其实就是删除各个节点的逻辑关系、释放各个节点的占用内存,并且释放根节点。
  清空一个树,就是在保留根节点的基础上,删除逻辑关系、释放内存。
  所以,代码可以总结为这样。

/**
 * 功 能:
 *      二叉树节点释放
 * 参 数:
 *      tree:要释放的二叉树
 * 返回值:
 *      无 
 **/
void BiTree_Release(BiTreeNode *tree)
{
    if (tree == NULL)
        return;

    // 递归释放左孩子节点
    if (tree->lchild != NULL)
    {
        BiTree_Release(tree->lchild);
        tree->lchild = NULL;
    }

    // 递归释放右孩子节点
    if (tree->rchild != NULL)
    {
        BiTree_Release(tree->rchild);
        tree->rchild = NULL;
    }

    // 释放根节点
    if (tree != NULL)
        tree = NULL;
}

/**
 * 功 能:
 *      销毁一个树
 * 参 数:
 *      tree:要操作的树
 * 返回值:
 *      无
 **/
void BiTree_Destroy(BiTree *tree) // O(1)
{
    if (tree == NULL)
        goto ERROR;

    TBiTree *BiTree = (TBiTree *)tree;

    BiTree_Release(BiTree->root);

ERROR:
    return;
}

/**
 * 功 能:
 *      清空一个树
 * 参 数:
 *      tree:要操作的树
 * 返回值:
 *      无
 **/
void BiTree_Clear(BiTree *tree) // O(1)
{
    TBiTree *BiTree = (TBiTree *)tree;

    if (BiTree != NULL)
    {
        BiTree_Release(BiTree->root);
        BiTree->count = 0;
        BiTree->root = NULL;
    }
}

九、度、根、深度、节点个数等

   好吧,没有什么可以进行描述的。关于这些的基础知识,可以详细参考博文:

  数据结构(十) – C语言版 – 树 - 基础知识

  数据结构(十一) – C语言版 – 树 - 二叉树基本概念

  对于叶子节点个数、深度等常规使用的代码,详细可以参考博文:

  数据结构(十四) – C语言版 – 树 - 二叉树的叶子节点、深度、拷贝等

  本博文中使用的实现的思路是一样的,代码方面稍微有一些区别,下面就给出代码。

/**
 * 功 能:
 *      计算树中的节点的个数( >=1 )
 * 参 数:
 *      root :要操作的树
 * 返回值:
 *      成功:节点的个数
 *      失败:0
 **/
static int BiTree_NodesCnt(BiTreeNode *root) // O(n)
{
    int nodes = 0;

    if (root == NULL)
        goto END;

    nodes = BiTree_NodesCnt(root->lchild) + BiTree_NodesCnt(root->rchild) + 1;

END:
    return nodes;
}

/**
 * 功 能:
 *      计算树的度
 * 参 数:
 *      root :要操作的树
 * 返回值:
 *      成功:树的度
 *      失败:0
 **/
static int __BiTree_Degree(BiTreeNode *root) // O(n)
{
    int degree = 0;

    if (root == NULL)
        goto ERROR;
    /* 存在左子树,度加1 */
    if (root->lchild != NULL)
        degree++;

    /* 存在右子树,度加1 */
    if (root->rchild != NULL)
        degree++;

    /**
     *  如果度为1,说明只有一个子树(或左或右),那还需要判断子树的度是多少
     *  如果度不为1,则说明两种情况
     *      1.既存在左子树也存在右子树,此时度为2,为二叉树的最大的度
     *      2.既不存左子树在也不存在右子树,此时度为0
     **/
    if (degree == 1)
    {
        int leftDegree = __BiTree_Degree(root->lchild);
        int rightDegree = __BiTree_Degree(root->rchild);

        degree = degree > leftDegree ? degree : leftDegree;
        degree = degree > rightDegree ? degree : rightDegree;
    }

ERROR:
    return degree;
}

/**
 * 功 能:
 *      计算树的深度
 * 参 数:
 *      root :要操作的树
 * 返回值:
 *      成功:树的深度
 *      失败:0
 **/
static int __BiTree_Depth(BiTreeNode *root) // O(n)
{
    int depth = 0;

    if (root != NULL)
    {
        int leftDepth = __BiTree_Depth(root->lchild);
        int rightDepth = __BiTree_Depth(root->rchild);

        depth = ((leftDepth > rightDepth) ? leftDepth : rightDepth) + 1;
    }

    return depth;
}

/**
 * 功 能:
 *      获取树中的根节点
 * 参 数:
 *      tree :要操作的树
 * 返回值:
 *      成功:获取后的树的节点
 *      失败:NULL
 **/
BiTreeNode *BiTree_Root(BiTree *tree) // O(1)
{
    TBiTree *BiTree = (TBiTree *)tree;
    return BiTree == NULL ? NULL : BiTree->root;
}

/**
 * 功 能:
 *      获取树中的深度/高度
 * 参 数:
 *      tree :要操作的树
 * 返回值:
 *      成功:获取后的树的节点
 *      失败:NULL
 **/
int BiTree_Depth(BiTree *tree) // O(n)
{
    TBiTree *BiTree = (TBiTree *)tree;

    return BiTree == NULL ? -1 : __BiTree_Depth(BiTree->root);
}

/**
 * 功 能:
 *      获取树中的节点的个数
 * 参 数:
 *      tree :要操作的树
 * 返回值:
 *      成功:节点的个数
 *      失败:-1
 **/
int BiTree_Count(BiTree *tree) // O(1)
{
    TBiTree *BiTree = (TBiTree *)tree;

    return BiTree == NULL ? -1 : BiTree->count;
}
/**
 * 功 能:
 *      获取树中的度
 * 参 数:
 *      tree :要操作的树
 * 返回值:
 *      成功:树的度
 *      失败:-1
 **/
int BiTree_Degree(BiTree *tree) // O(n)
{
    TBiTree *BiTree = (TBiTree *)tree;

    return BiTree == NULL ? -1 : __BiTree_Degree(BiTree->root);
}

十、案例测试

   上面写的天花乱坠,贴出了代码也显得牛逼轰轰的样子,但是没有实际测试效果展示的话,天知道你TM的代码是好用还是不好用,是黄金还是狗S还是需要贴出实际测试效果才能说明至少可以使用。那么下面就直接整了。。。。。

10.1、工程目录结构

  为了兼容unixwindows系统以及方便进行工程管理,特意使用Cmake工具进行编译等,关于Cmake的环境搭建,详细可以参考博文:

  windows – 使用VSCode + CMake + MinGW搭建C/C++编译、调试、运行环境

  本次测试中测试工程的目录结构如下所示。

biTree-create-general/
├── CMakeLists.txt
├── README.md
├── build
├── image
│   └── image.jpg
├── main
│   └── main.c
├── runtime
└── src
    └── biTree
        ├── BiTree.c
        └── BiTree.h

6 directories, 6 files

10.2、测试案例代码

  前面已经说明整体工程的结构,以及需要的文件,下面是测试底层功能函数的测试demo。是呀,好听了就叫测试案例,其实就是 main 函数所在文件,那么main.c文件内容如下所示。

#include "../src/biTree/BiTree.h"
#include <stdio.h>
#include <stdlib.h>

extern TFBiTree fBiTree;

typedef struct __Node
{
    BiTreeNode header; /* 头指针 */
    char v;            /* 自定义的数据域 */
} Node;

void printf_data(BiTreeNode *node)
{
    if (node != NULL)
        printf("%c ", ((Node *)node)->v);
}

int main(int argc, char *argv[])
{
    BiTree *tree = fBiTree.create();

    Node n1, n2, n3, n4, n5, n6;

    n1.v = 'A';
    n1.header.lchild = NULL;
    n1.header.rchild = NULL;
    n2.v = 'B';
    n2.header.lchild = NULL;
    n2.header.rchild = NULL;
    n3.v = 'C';
    n3.header.lchild = NULL;
    n3.header.rchild = NULL;
    n4.v = 'D';
    n4.header.lchild = NULL;
    n4.header.rchild = NULL;
    n5.v = 'E';
    n5.header.lchild = NULL;
    n5.header.rchild = NULL;
    n6.v = 'F';
    n6.header.lchild = NULL;
    n6.header.rchild = NULL;

    fBiTree.insert(tree, (BiTreeNode *)&n1, 0, 0);
    fBiTree.insert(tree, (BiTreeNode *)&n2, 0x00, 1);
    fBiTree.insert(tree, (BiTreeNode *)&n3, 0x01, 1);
    fBiTree.insert(tree, (BiTreeNode *)&n4, 0x00, 2);
    fBiTree.insert(tree, (BiTreeNode *)&n5, 0x02, 2);
    fBiTree.insert(tree, (BiTreeNode *)&n6, 0x02, 3);

    printf("Height = %d, Degree = %d, Count = %d\n",
           fBiTree.depth(tree), fBiTree.degree(tree), fBiTree.count(tree));

    printf("Position At (0x02, 2): %c\n", ((Node *)fBiTree.get(tree, 0x02, 2))->v);

    printf("Prev Older: ");
    fBiTree.display(tree, printf_data, 0);
    printf("in   Older: ");
    fBiTree.display(tree, printf_data, 1);
    printf("post Older: ");
    fBiTree.display(tree, printf_data, 2);
    printf("layerOlder: ");
    fBiTree.display(tree, printf_data, 3);

    printf("\nAfter Insert G: \n");
    Node n7;
    n7.v = 'G';
    n7.header.lchild = NULL;
    n7.header.rchild = NULL;
    fBiTree.insert(tree, (BiTreeNode *)&n7, 0x01, 1);
    printf("Height = %d, Degree = %d, Count = %d\n",
           fBiTree.depth(tree), fBiTree.degree(tree), fBiTree.count(tree));

    printf("Prev Older: ");
    fBiTree.display(tree, printf_data, 0);
    printf("in   Older: ");
    fBiTree.display(tree, printf_data, 1);
    printf("post Older: ");
    fBiTree.display(tree, printf_data, 2);
    printf("layerOlder: ");
    fBiTree.display(tree, printf_data, 3);

    Node n8;
    n8.v = 'H';
    n8.header.lchild = NULL;
    n8.header.rchild = NULL;

    printf("\nAfter Update B->H: \n");
    fBiTree.update(tree, 0x00, 1, (BiTreeNode *)&(n8));

    printf("Prev Older: ");
    fBiTree.display(tree, printf_data, 0);
    printf("in   Older: ");
    fBiTree.display(tree, printf_data, 1);
    printf("post Older: ");
    fBiTree.display(tree, printf_data, 2);
    printf("layerOlder: ");
    fBiTree.display(tree, printf_data, 3);

    fBiTree.delete(tree, 0x00, 1);

    printf("\nAfter Delete B: \n");
    printf("Height = %d, Degree = %d, Count = %d\n",
           fBiTree.depth(tree), fBiTree.degree(tree), fBiTree.count(tree));

    printf("Prev Older: ");
    fBiTree.display(tree, printf_data, 0);
    printf("in   Older: ");
    fBiTree.display(tree, printf_data, 1);
    printf("post Older: ");
    fBiTree.display(tree, printf_data, 2);
    printf("layerOlder: ");
    fBiTree.display(tree, printf_data, 3);

    fBiTree.clear(tree);

    printf("\nAfter Clear: ");
    printf("Height = %d, Degree = %d, Count = %d\n",
           fBiTree.depth(tree), fBiTree.degree(tree), fBiTree.count(tree));

    fBiTree.destroy(tree);

    return 0;
}

  上面代码中创建了一个树,并且在树中分别插入 n1、n2、…、n6 6个节点,那么为了更加清晰的结合代码查看,下面就代码创建的树结构图用下面图片分别来表示。

在这里插入图片描述

图10.1 代码中创建并操作的树的结构图
  

10.3、测试效果显示

  本次测试是在windows环境下进行,其他系统等详细说明在README.md中查看。
  1、使用Cmake编译,使用下面指令

cd build
cmake -G"MinGW Makefiles" ../ # 注意 ../ ,表示上级目录
make

在这里插入图片描述

图10.2 实际编译效果

  
  2、经过cmake编译之后,配置cmake可执行文件放在固定目录runtime下,可以使用在当前目录下使用指令 ./../biTree.exe 来运行可执行程序,也可以进入到目录runtime中,然后使用指令 ./biTree.exe ,即可运行测试程序。实际测试的结果如下图所示。
  
在这里插入图片描述

图10.3 代码执行效果图

  
  好啦,废话不多说,总结写作不易,如果你喜欢这篇文章或者对你有用,请动动你发财的小手手帮忙点个赞,当然关注一波那就更好了,好啦,就到这儿了,么么哒(*  ̄3)(ε ̄ *)。
在这里插入图片描述

上一篇:数据结构(十四) – C语言版 – 树 - 二叉树的叶子节点、深度、拷贝等
下一篇:数据结构(十六) – C语言版 – 树 - 二叉树的线索化及遍历 – 左指针域线索化、顺序表线索化、链表线索化

猜你喜欢

转载自blog.csdn.net/zhemingbuhao/article/details/106317182