C语言实现树和二叉树(二)

news2024/11/28 3:16:00

       OK,各位未来的高级程序员们,大家好,今天我们来讲解一下二叉树这个部分的第二种存储结构---------链式存储结构,相信大家对这部分内容已经很期待了,但是,这里我们在开讲之前,要先补充一个知识,这个知识本应该是出现在前面一篇博客的内容,之前由于种种原因,就没有写上,那么,现在在这里给大家补上,请谅解。

补充:TOP-K问题:N个数中找最大的前K个。

方法1:建立一个N个数的大堆,空间复杂度:O(N),

             然后再PopK次               时间复制度:O(K*logN)。

       方法1的这种办法是我们大家最容易想到的一种办法,这个方法我们虽然看不到有什么问题,但是大家不妨想一想,如果N是10亿呢,这种情况下我们再去看方法1,是不是就有点问题了,一下开创10亿个空间,这样的话空间就太大了,空间无法得到一个很好的使用,甚至还会造成一定的空间浪费的情况,因此,我们在这里不推荐使用方法1,用方法2:

方法2:我们可以先将N个数(这里我们将这N个数放在一个文件中,这里我们决定采用文件的方法来实现)中的前K个数拷贝到程序中去,让其形成一个小堆(这里我们采用的是建小堆的方式来完成TOP-K问题的),将这K个数建好一个小堆之后,我们这时候再从文件的第K+1个数开始往后遍历,让它们分别与刚刚建成的小堆的堆顶元素进行比较,若比堆顶元素小,让文件中的下一个元素进行比较,反之,则这个元素替代堆顶元素,成为新的堆顶元素,由于是小堆,必须遵守小堆的规则,采用向下排序建堆的方法来确定这个新的堆顶元素的真正位置,当文件中的所有元素全部比较完之后,这样,小堆中的K个元素就是N个数中最大的前K个数了。

       这里再给大家说一下我在第一次接触这个知识的时候,我的一个疑问,就是说如果N个数中的第4大的元素挡在堆顶咋办?那么这样的话,数都进不去了,其实按照我们讲的方法2来解决TOP-K问题的话是不会存在这样的问题的,因为前K个数建立的是一个小堆,小堆的特点是堆顶元素是最小的元素,如果N个数中的第4大的元素挡在堆顶的话,就足以说明N个数中的第1大的元素,第2大的元素,第3大的元素均已经在小堆中了,因此,我们不需要去担心这种情况的发生。

(1).建立数据:

创建1000000个数据并将这1000000个数据全部放在"deta.txt"这个文件中。 

void CreateData()
{
    int n = 1000000;
    srand(time(0));//这里我们使用rand函数来生成1000000个随机数据,由于rand函数生成的数不是真正的随机数,我们这里给其一个种子,大家如果有还不知道rand这个函数的话,可以通过这个链接去了解一下这个函数。
    const char* file = "deta.txt";
    FILE* fin = fopen(file, "w");//打开文件,并设置成"w"的模式。
    if (fin == NULL)
    {
        perror("fopen error");
        return 0;
    }
    for (size_t i = 0; i < n; ++i)
    {
        int x = rand() % 1000000;//随机生成1000000个数据。
        fprintf(fin, "%d\n", x);//用fprintf函数将这1000000个数据一一写入到"deta.txt"这个文件中。
    }
    fclose(fin);//关闭文件。
}

​

rand函数:rand - C++ Referencehttps://legacy.cplusplus.com/reference/cstdlib/rand/?kw=rand

fprint函数:fprintf - C++ Referencehttps://legacy.cplusplus.com/reference/cstdio/fprintf/?kw=fprintf

大家可以通过上面的两个链接去了解一下这两个函数。

(2).完成TOP-K:

void TopK()
{
    int k = 10;//我们这里选出N个数据中的最大的前10个元素。
    int* tmp = (int*)malloc(sizeof(int) * k);//开创10个空间的数组。
    if (tmp == NULL)//判断数组是否开创成功。
    {
        perror("malloc fail");
        return;
    }
    const char* file = "deta.txt";
    FILE* fout = fopen(file, "r");//打开文件,并设置成"r"的模式。
    for (int i = 0; i < k; i++)//通过上面我们讲的逻辑,这里我们要将上面的那个函数中生成的1000000个数中的前10个元素写入到"deta.txt"文件中。
    {
        fscanf(fout, "%d", &tmp[i]);//这里我们使用fscanf函数来向数组中写入元素。
    }
    for (int i = (k - 1 - 1) / 2; i >= 0; i--)//将数组中的10个元素排列成小堆。
    {
        AdjustDown(tmp, k, i);
    }
    int x = 0;
    while (fscanf(fout, "%d", &x) > 0)//我们将剩下的元素依次与堆顶元素进行比较。
    {
        if (x > tmp[0])//如果有某个元素比堆顶元素大,这个元素就替代堆顶元素,成为新的堆顶元素,并建成新的小堆。
        {
            tmp[0] = x;//替代堆顶元素。
            AdjustDown(tmp, k, 0);//建成新的小堆。
        }
    }
}

       好的,以上就是我们补充前面的内容,接下来,我们现在开始进入我们今天的正题:链式存储结构。

       一.链式存储结构:

接下来我们要讲的内容它适用于不完全二叉树结构,也就是普通的二叉树结构。

       1.遍历:(1).前序遍历:根    左子树    右子树(要求任意一棵树都按照这样的方式去访问)

bbdb97ca596241169be6789c9e0f2566.png

上面的这幅图就是前序遍历的过程图:图有点抽象,我来为大家解释一下:

a80dfbdde9b34aefafc6da1eebae040f.png

前序遍历的过程其实并不难,就如上图所画的那样,将二叉树分为根,左子树,右子树后,再分别将左右子树均看成一棵树,再分,直到遇到空指针就可以停下了,这个遍历过程其实就和套娃是一个道理,要完成这个过程靠的是递归的思想,当我们完成这一步时(也就是我们遇到空指针的时候),就会返回到上一个递归这个函数的位置,大家一定要了解递归的全过程,递归是我们必须要掌握的一个知识,它的重要程度堪比5颗星。

       (下面的操作可能会有一段繁琐,但是只要大家认认真真看,相信就绝对可以理解)

       为了方便大家的理解,我在这里给大家细细的过一遍,过一遍之后,那么后面的中序遍历和后序遍历就不细细的写了,过程基本和前序遍历相同,前序遍历在访问的过程中,我们先访问的是根,也就是1,然后再访问左子树,对于左子树,我们也是相同的操作,先访问根,也就是2,然后再访问以2为根的这棵子树的左子树,也是相同的操作,先访问根,也就是3,然后再访问3为根的这棵子树的左子树,由于以3为根的这棵子树的左子树为空,无法再继续进行拆分,因此,递归结束,返回,再访问3为根的这棵子树的右子树,由于访问3为根的这棵子树的右子树也为空,无法再继续进行拆分,因此,递归结束,返回,那么,到这里,就说明我们将以2为根的这棵子树的左子树全部都访问完了,接下来,我们就可以来访问以2为根的这棵子树的右子树了,由于以2为根的这棵子树的有子树为空,无法再继续进行拆分,因此,递归结束,返回,到这里,就说明我们将以1为根的这棵子树的左子树全部都访问完了,接下来,我们就可以来访问以1为根的这棵子树的右子树了,访问右子树,也是相同的操作,先访问根,也就是4,然后再访问4为根的这棵子树的左子树,也就是5,然后再访问以5为根的这棵子树的左子树,也是相同的操作,由于以5为根的这棵子树的左子树为空,无法再继续进行拆分,因此,递归结束,返回,到这里,就说明我们将以4为根的这棵子树的左子树全部都访问完了,接下来,我们就可以来访问以4为根的这棵子树的右子树了,先访问根,也就是6,然后再访问6为根的这棵子树的左子树,由于以6为根的这棵子树的左子树为空,无法再继续进行拆分,因此,递归结束,返回,接下来,我们就可以来访问以6为根的这棵子树的右子树了,访问右子树,由于以6为根的这棵子树的右子树为空,无法再继续进行拆分,因此,递归结束,返回,到这里,就说明我们将以4为根的这棵子树的右子树全部都访问完了,至此,以1为根的这棵子树的右子树全部都访问完了,遍历结束。

8cac9e8e3fcb40eaa4b7a576658e6db8.png

(N表示的是NULL)。

       (2).中序遍历:左子树       根       右子树

1fc2a978eb394f68808a9a577d065c36.png

       (3).后序遍历:左子树       右子树       根

7a70d24bb3b84bbbb16a869f4b157ffa.png

       2.实现二叉树中一些对应的各个操作:

       (1).定义一个二叉树的类型(实际上就是定义一个结构体,因为是二叉树,每一个节点都有两个指针,一个指针指向左子树,另一个指针指向右子树,另外还需要一个存放元素的变量。):

typedef int BTDataType;
typedef struct BinaryTreeNode
{
    BTDataType _data;//定义一个变量,来存放数据。
    struct BinaryTreeNode* _left;//指向左子树的指针。
    struct BinaryTreeNode* _right;//指向右子树的指针。
}BTNode;

       (2).创建二叉树的一个节点:
       我们这里之所以写这一步操作,是因为后面我们在进行二叉树的插入操作时,需要建立节点。

BTNode* BuyNode(int x)
{
    BTNode* node = (BTNode*)malloc(sizeof(BTNode));//建立一个新的节点。
    if (node == NULL)//判断是否建立节点成功。
    {
        perror("malloc fail");
        return NULL;
    }

    node->_data = x;//我们这个函数传过来的参数就是我们要向开创的新的节点中存放的值。
    node->_left = NULL;//指针一般我们都将其初始化为NULL。
    node->_right = NULL;

    return node;
}

       (3).二叉树节点的初始化:

void BinaryTreeInit(BTNode* root)
{
    assert(root);//首先要看一下root指针是否接收到了传过来的地址。
    root->_data = 0;
    root->_left = NULL;
    root->_right = NULL;
}

       (4).二叉树前序遍历:

       通过上述的讲解过程我们可以知道,前序遍历我们这里是通过递归操作来实现的。

void BinaryTreePrevOrder(BTNode* root)
{
    if (root == NULL)//既然知道我们这里进行的是递归操作,那么就必须得有递归的结束条件,由上可知,递归的结束条件就是我们遇到空树的时候,这一趟递归就可以结束了。
    {
        printf("N ");
        return;
    }
    printf("%d ", root->_data);//因为我们的这一步操作实现的是前序遍历,前序遍历的访问顺序是根然后是左子树再然后是右子树,因此,我们这里先访问根。
    BinaryTreePrevOrder(root->_left);//再去遍历左子树。
    BinaryTreePrevOrder(root->_right);//最后去遍历右子树。
}

       (5).二叉树中序遍历:

void BinaryTreeInOrder(BTNode* root)
{
    if (root == NULL)//既然知道我们这里进行的是递归操作,那么就必须得有递归的结束条件,由上可知,递归的结束条件就是我们遇到空树的时候,这一趟递归就可以结束了。
    {
        printf("N ");
        return;
    }
    BinaryTreePrevOrder(root->_left);//因为我们的这一步操作实现的是中序遍历,中序遍历的访问顺序是左子树然后是根再然后是右子树,因此,我们这里先访问左子树。
    printf("%d ", root->_data);//然后再访问根。
    BinaryTreePrevOrder(root->_right);//最后再访问右子树。
}

       (6).二叉树后序遍历:

void BinaryTreePostOrder(BTNode* root)
{
    if (root == NULL)//既然知道我们这里进行的是递归操作,那么就必须得有递归的结束条件,由上可知,递归的结束条件就是我们遇到空树的时候,这一趟递归就可以结束了。
    {
        printf("N ");
        return;
    }
    BinaryTreePrevOrder(root->_left);//因为我们的这一步操作实现的是后序遍历,后序遍历的访问顺序是左子树然后是右子树再然后是根,因此,我们这里先访问左子树。
    BinaryTreePrevOrder(root->_right);//然后我们再去访问右子树。
    printf("%d ", root->_data);//最后我们访问根。
}

       (7).二叉树节点个数:

int BinaryTreeSize(BTNode* root)
{
    if (root == NULL)//既然知道我们这里进行的是递归操作,那么就必须得有递归的结束条件,由上可知,递归的结束条件就是我们遇到空树的时候,这一趟递归就可以结束了。
    {
        return 0;//我们这里研究的是二叉树的节点个数,递归结束的时候,说明这棵树是空树,也就没有节点,返回0。
    }
    return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;//如果我们现在访问的这棵树不是空树的话,那么我们就去找这棵树的左子树的节点和这棵树的右子树的节点,并且在最后我们在向上一层返回时还需要加1,这个1代表的是这棵树的根节点。
}

37b71d9c21504bbe8e96e986faf6e71f.png

我们就拿红色圈圈里面的这棵子树来说明一下为什么要加1,return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;这一步程序中BinaryTreeSize(root->_left)得到的是以4为根的这棵树的左子树的节点个数,BinaryTreeSize(root->_right)得到的是以4为根的这棵树的右子树的节点个数,当得到之后,我们就要递归回到调用以1为根的这棵树的右子树的函数了,这个函数得到的是以1为根的树的右子树的节点个数,如果我们不加1直接返回的话,就没有加上以4为根的这棵树的那个根节点的个数,因此,这里我们要加1,而这个1表示的就是根节点的个数。

       (8).二叉树的层数:

       这一个步骤中,我们要实现计算二叉树的层数,在讲解开始之前,我们要先知道二叉树的层数是取决于这棵树的左右两个子树的层数中最大的那一个,就比如说,某一棵树的左子树的层数是4,右子树的层数是2,那么这个二叉树的层数就是4层,或者说,层数就是高度,二叉树的高度这个概念我在上一个博客中有讲到,大家有不了解的,可以到上一个博客去了解一下,链接在下面。树和二叉树系列(一):-CSDN博客文章浏览阅读590次,点赞17次,收藏27次。这里我们还是和上面一样来为大家解释一下过程,我们大家都是学习数据结构的,相比大家肯定都或多或少的通听过堆排序吧,每错,接下来,我就为大家来介绍一下堆排序的过程,说起堆排序,想必大家的脑海中第一个印象就是建立一个第三方数组,来帮助进行排序(我们要想实现堆排序,首先就得建立一个堆),这样也是可以的,但是这样的话,会让空间复杂度上升的,我们这里有更好的方法可以去实现堆排序,就是我们直接在原数组上建堆。也就是这一个篇章,树和二叉树的精彩讲解。https://blog.csdn.net/2301_81390458/article/details/139456368?spm=1001.2014.3001.5501

int BinaryTreeLevelSize(BTNode* root)
{
    if (root == NULL)//如果这棵树为空树的话,就说明这棵树是没有节点的,那么这棵树的层数自然就是0了,因此,我们这里直接返回0就可以了。
    {
        return 0;
    }
    int a1 = BinaryTreeLevelSize(root->_left);//因为我们这里是要计算某一棵二叉树的层数,我们就要得到这棵二叉树的左右这两棵子树的层数,这里我们先得到左子树的层数,定义一个int类型的变量去接收左子树的层数。
    int a2 = BinaryTreeLevelSize(root->_right);//再定义一个int类型的变量去接收右子树的层数。
    return a1 > a2 ? a1 + 1 : a2 + 1;//如果这棵树不是空树的话,那么我们完成上面的操作后就要返回这棵树的层数了,根据我们上面的分析,我们这里选择左右子树层数大的那一个层数作为这棵树的层数去返回,由于上面我们得到的是该棵树的左右子树的层数,并不是这棵树的层数,因此,我们这里在找出最大值后还要加上这棵树的根(因为根的层数是1),也就是加1。
}

这个步骤到这里其实还还没有结束,这里我们在来给大家说明一个容易出现的问题,就是最后的那三步可不可以压缩成一句代码,如下所示:

return BinaryTreeLevelSize(root->_left) > BinaryTreeLevelSize(root->_right) ? BinaryTreeLevelSize(root->_left) + 1 : BinaryTreeLevelSize(root->_right) + 1;

好的,这里给大家说一下,这样写的话,按道理来说的话是正确的,但是这样写的话,会有一个很大的错误,就是这样做的话,会导致时间复杂度将会大幅度上升,效率可能会降低,我们现在来分析一下这一句代码,现调用BinaryTreeLevelSize(root->_left) > BinaryTreeLevelSize(root->_right)这两个函数得到这棵树的左右子树的层数,然后比较,比较完了之后,程序就会接着往后面去走,去让大的那个层数加1,但是,不知道大家有没有注意到,就是最大的那个层数的值没有被我们保存下来,就是丢了,那么我们到后面还要再次去调用这个函数求一下这个最大的层数值,说到这里,大家可能会觉得这没有什么,但是,大家好像忘了一点,就是,我们这个是通过递归来实现的,每次都要去递归一下,这个所消耗的时间是非常恐怖的,你若不信的话,可以自己画图试一试。

       (9).二叉树叶子节点个数:

        这一个步骤我们要计算的是某一棵二叉树的叶子节点(前一个博客有讲)的个数,也就是这里我们要求的是没有孩子的节点,这里我们要先了解一下叶子节点的特点,就是这棵树的左右子树均为空树,那么这棵树对应的根节点就是叶子节点。

int BinaryTreeLeafSize(BTNode* root)
{
    if (root == NULL)//如果这棵树是空树的话,则这棵树没有叶子节点,返回0即可。
    {
        return 0;
    }
    if (root->_left == NULL && root->_right == NULL)//判断一下这棵树的左右节点是否均为空,如是的话,则说明这棵树对应的根节点为叶子节点,这棵树有1个叶子节点,返回1即可。
    {
        return 1;
    }
    return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);//如果这棵树不是空树的话,就返回这棵树的左右子树中所含有的叶子节点的个数之和。
}

        (10).二叉树第k层节点个数:

        我们有时候会想要求一下某一棵二叉树中的某一层上的节点个数,我们大家有好多的人第一次见到这道题的时候是没有思路的,这里我来为大家提供一种思路,以下面这一颗树为例给大家解释一下:

bde0893cd52b41c18ee2372f2fab7864.png

比如说我现在想要得到第3层的节点个数,那么对于以1这个节点为根的这棵树来说的话,就是求第3层的节点个数,但是,对于以2,4这两个节点为根的这两棵树来说的话,求的就是第2层的节点个数,那么对于以3,5,6这三个节点为根的这三棵树来说的话,就是求第1层的节点个数,换句话说,就是求这三棵树的根结点个数。
 

int BinaryTreeLevelKSize(BTNode* root, int k)
{
    if (root == NULL)//如果这颗树为空树,就说明第k层上的节点个数为0。
    {
        return 0;
    }
    if (k == 1 && root != NULL)//如果k==1就说明我们要求的是这一层的节点个数,同时我们还要限制这棵树不能为空树,均符合这两个条件的话,那么,就说明有一个节点,返回1即可。
    {
        return 1;
    }
    return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);//若程序进行到这里时,就说明我们还没有到第k层,就接着去找这棵树的左右子树,这样的话,将这棵树的左右子树再次看成一棵新的树,那么对于这棵新的树来说,要找的就是第k-1层的节点个数。
}

       (11).二叉树查找值为x的节点:

        这一步操作就类似于我们之前学过的查找元素的操作,这个二叉树的查找和前面不同的是,二叉树中的许多个节点中可能存放的都是同一个值,因此,这里我们返回找到的第一个节点的地址即可。

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
    if (root == NULL)//如果这颗树为空树,则说明没有找到该节点,返回NULL即可。
    {
        return NULL;
    }
    if (root->_data == x)//如果这个节点中存储的值刚好就是我们要查找的值,那么我们返回这个节点的地址即可。
    {
        return root;
    }
    BTNode* left = BinaryTreeFind(root->_left, x);//当程序走到这里时,说明这棵树的根节点不是我们要找的节点,那么我们就到这棵树的左右子树上去找,先去左子树找。
    BTNode* right = BinaryTreeFind(root->_right, x);//然后再去到右子树去找。
    return left == NULL ? right : left;//我们这里返回的是找到的那个节点的地址,如果在左子树上找到了,右子树上没有找到,返回左子树找到的那个节点的地址,如果在右子树上找到了,左子树上没有找到,返回右子树找到的那个节点的地址,如果再左右子树上都找到了,那么,随便返回那个都可以,因为我们这个题的目的是找到符合条件的节点,只要找到就可以了。
}

       (12).二叉树销毁:

       二叉树的销毁这里我们采用的是后序遍历的方法去销毁的,这里其实使用这个方法销毁是最好的方法,如果我们先将根节点销毁掉,那么,就无法找到它的左右子树,还得在销毁之前将它的左右子树先存起来,有点麻烦,因此,我们在这里不采用先销毁根节点的这种方法。

void BinaryTreeDestory(BTNode** root)//这里传不传地址其实都可以,都可以将其销毁掉。
{
    if (*root == NULL)//如果这棵树为空树,则不需要将其销毁掉,因为空树就是啥也没有。
    {
        return;
    }
    BinaryTreeDestory(&((*root)->_left));//先销毁左子树。
    BinaryTreeDestory(&((*root)->_right));//再销毁右子树。
    free(*root);//如果程序走到这一步,说明左右子树均被销毁掉了,这一步是销毁掉根节点。
}

       (13).层序遍历:

       这一步我们要进行的层序遍历就是一层一层地去遍历二叉树,我们这里在进行二叉树的层序遍历的时候,还需要借用到队列的相关知识(我们这里在写层序遍历是不需要将空指针也入队列)。

3fe486ee47464081a57bd6a427f58075.png

接下来我就上面的二叉树来给大家讲解一下层序遍历的过程,正如上图所示的那样,我们这里先将1这个节点入队,由于是层序遍历,接下来就要入1这个节点的两个子树的根,也就是2这个节点和4这个节点,先将1这个节点出队列,然后将1这个节点的的两个子树的根节点入队列,也就是2这个节点和4这个节点入队列,将2这个节点出队列,然后将2这个节点的的子树的根节点入队列,也就是3这个节点入队列,将4这个节点出队列,然后将4这个节点的的两个子树的根节点入队列,也就是5这个节点和6这个节点,将5这个节点出队列,然后将5这个节点的的子树的根节点入队列,将6这个节点出队列,然后将6这个节点的的子树的根节点入队列,当队列为空时,就说明层序遍历结束了。

       这里我们在借助队列来实现层序遍历的时候还有一个要注意的地方,就是队列中的元素类型是啥,我们应该存什么?首先,我们在这里肯定是不能存节点中的值的,否则,找不到根节点的左右子树,其次,我们也不要存节点,这样消耗太大,既然如此,我们就只能存放指向该节点的指针。

void BinaryTreeLevelOrder(BTNode* root)
{
    Queue q;//我们在完成层序遍历的时候,需要借助队列的帮助,因此,我们这里要先定义一个队列。
    QueueInit(&q);//队列的初始化。
    if (root == NULL)//如果二叉树为空,则不需要遍历。
    {
        return;
    }
    QueuePush(&q, root);//先将根压到队列中去。
    while (!QueueEmpty(&q))//层序遍历的结束条件是队列不为空。
    {
        BTNode* front = QueueFront(&q);//得到队列中的首元素。
        printf("%d ", front->_data);//打印出该节点存储的值。
        QueuePop(&q);//将这个首元素出队列。
        if (front->_left != NULL)//判断该根节点的左节点是否为空,若不为空,入队列,否则,不如队列。
        {
            QueuePush(&q,front->_left);
        }
        if (front->_right != NULL)//判断该根节点的右节点是否为空,若不为空,入队列,否则,不如队列。
        {
            QueuePush(&q, front->_right);
        }
    }
    QueueDestroy(&q);//销毁队列。
}

       (14).判断二叉树是否是完全二叉树:

        这一步我们判断一个二叉树是否是完全二叉树我们还需要借助队列的帮助,在讲述之前,我们要先了解一下完全二叉树的判断条件,要想判断一棵二叉树是否为完全二叉树,就要看一下两个节点之间是否有空指针(因此,我们在实现这个操作时,必须要将NULL也入队列),那照上着上面的那个思路,换句话说,就是我们可以按照层序遍历的过程,当我们遇到第一个NULL时就停止遍历,接下来就判断队列中剩下的元素中有没有不为NULL的元素,若有,则说明该二叉树不是完全二叉树,反之,则说明该二叉树是完全二叉树。

0496765622774fa0882bf4f447293a6d.png

(N代表的是NULL)

int BinaryTreeComplete(BTNode* root)
{
    Queue q; 我们在完成层序遍历的时候,需要借助队列的帮助,因此,我们这里要先定义一个队列。
    QueueInit(&q); //队列的初始化。
    if (root == NULL)//判断一下该二叉树是否为空树,若是空树,则说明此二叉树就是完全二叉树。
    { 
        return 1;
    }
    QueuePush(&q, root); //先将根压到队列中去。
    while (!QueueEmpty(&q)) //我们这里要使用到层序遍历的思想,层序遍历的结束条件是队列不为空。
    {
        QDataType front = QueueFront(&q);
        QueuePop(&q);
        if (front == NULL)//如果为NULL,则就停止遍历二叉树,接下来就去判断队列中剩下的元素中有没有不为NULL的元素。
        {
            break;
        }
        QueuePush(&q, front->_left); 
        QueuePush(&q, front->_right); 
    }
    while (!QueueEmpty(&q))//我们要遍历队列中剩余的元素,因此循环结束条件是队列不为空。
    {
        BTNode* front = QueueFront(&q);//得到队首元素。
        QueuePop(&q);//队首元素出队列。
        if (front != NULL)//判断队首元素是否为空。
        {
            return 0;
        }
    }
    QueueDestroy(&q);//销毁队列。
    return 1;
}

(上述代码中返回0说明该二叉树不是完全二叉树,返回1说明该二叉树是完全二叉树)

        (15).通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树(#代表的是NULL):

        这个步骤是想让我们将上面给的这个数组将它转换成为一棵二叉树,换句话说,就是上面给的这个数组是某一棵二叉树的前序遍历的结果,现在需要我们将这个数组给转换回二叉树去。

BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)//这里为了方便大家的理解,我先来为大家说明一下这三个参数分别所代表的含义都是什么,a是某一棵二叉树的前序遍历的结果(使用数组表示),n是数组的长度(这里我们没有用上,后面我会向大家解释为什么有这个参数),pi指向的是访问数组的那个下标的地址,就是我们在访问数组时是通过下表来访问数组的,而pi指向的就是这个下标的地址。
{
    if (a[*pi] == '#')//如果我们当前访问的数组的位置是NULL,说明此处没有节点,就可以返回了。
    {
        (*pi)++;//(*pi)++这里之所以要写上这一是因为我们访问完数组中的这个元素后,下一次就要访问数组中的下一个元素了,因此,要++。
        return NULL;//返回NULL。
    }
    BTNode* root = (BTNode*)malloc(sizeof(BTNode));//如果此处不为NULL,则说明这里有一个节点,那么我们就要建立一个节点空间。
    if (root == NULL)
    {
        perror("malloc fail");
        return NULL;
    }
    root->_data = a[*pi++];//将我们要访问的数组元素放到我们刚刚开创的空间中,放完后,我们下一次访问的就是数组中的下一个元素了,因此要++。
    root->_left = BinaryTreeCreate(a, n, pi);//因为我们这个数组是二叉树通过前序遍历形成的,所以我们在返回创造二叉树时,也要遵循前序遍历来创建,因此,先接左子树。
    root->_right = BinaryTreeCreate(a, n, pi);//然后再接右子树。
    return root;//当程序走到这里时,说明我们已经将这棵二叉树给创建好了,那么,直接返回就可以了。
}

现在来给大家说一下刚刚上面遗留下来的两个问题:

       首先我们来解释一下为什么传的是当前我们要访问的数组元素的地址(也就是下标的地址),而不是下标的值,接下来,通过下图我来为大家解释一下(使用图中所示的数组来为大家解释):

c493248f3c4344d3b4a3e836b6a60e45.png

就上图来说,我们在第1次调用BinaryTreeCreate函数时,访问的是数组中下标为0的元素,也就是A,接下来,我们再调用BinaryTreeCreate函数,那么第2次调用该函数访问的元素应该是数组中下标为1的元素,也就是B,紧接着我们往后面走,就是第3次调用BinaryTreeCreate函数,下一个应该访问的是#这个元素,好的,按照我们上面写的程序,就是pi++,returnNULL,返回到第二次调用BinaryTreeCreate函数里面,接下来就是进行第4次调用BinaryTreeCreate函数,按照我们的逻辑来说,这次访问的数组中的元素应该是下标为3的这个元素,但是,实际上,我们发现,我们第4次调用BinaryTreeCreate函数中访问的数组元素居然是下标为2的这个元素,为啥呢?因为在第3次调用BinaryTreeCreate函数之后pi++的时候,他只是在第3次调用BinaryTreeCreate函数中++,形参改变,实参不变,因此导致后面的一些错误,所以,我们这里必须要传参。

       其次我们来解决一下下一个问题,就是参数中的n这个参数是干什么作用的,这个n其实就是数组的长度,之所以在参数中传这个数,是因为有时候题目中给你的那个数组不完整,就比如说,可能就会把上面的那个数组改成"ABD##E#H##CF##G#",最后会少一个#,那么这样造成的结果就是会出现越界的情况,加上这个是为了避免这种情况的出现,确定遍历的结束。

       这里还有一个情况需要提醒一下大家,就是if (a[*pi] == '#')这一行代码他不可以和下面的(*pi)++这行代码结合进而写成if (a[(*pi)++] == '#'),这种情况会出现问题的,如果我们访问的数组元素刚好是#,那这句代码没有问题,但是如果我们访问的元素不是#,就会出现问题,就拿这个数组来说吧(ab##c##),我们要访问的元素是a,但是程序是有先后顺序的,会先走这个a[(*pi)++]代码,走完后,pi指向的就是b这个元素了,但是我们按照逻辑走的话,这里应该是先访问a这个元素的,实际上,访问的是b这个元素,又出错的风险,因此,这两句代码是不可以结合的。

       到这里,我们的所有关于二叉树的部分就全部结束了,希望这篇博客上的内容可以帮助到大家,谢谢大家的支持。 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1844556.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AJAX 综合案例-day2

Bootstrap 弹框 功能&#xff1a;不离开当前页面&#xff0c;显示单独内容&#xff0c;供用户操作 步骤&#xff1a; 1. 引入 bootstrap.css 和 bootstrap.js 2. 准备 弹框标签 &#xff0c;确认结构 3. 通过 自定义属性 &#xff0c;控制弹框的 显示 和 隐藏 1. 通过属性…

SAP_FICO模块-获利能力段新增特征字段

业务背景&#xff1a; 公司有启用获利能力分析功能&#xff0c;有一个销售订单接口&#xff0c;是通过第三方销售订单管理平台推送数据到SAP的&#xff0c;用户希望对接新增一个编号ID到销售订单上&#xff0c;并且可以用KE24/KE30报表查看显示&#xff1b; 对于我这么一个后勤…

小程序打开空白的问题处理

小程序打开是空白的&#xff0c;如下&#xff1a; 这个问题都是请求域名的问题&#xff1a; 一、检查服务器域名配置了 https没有&#xff0c;如果没有&#xff0c;解决办法是申请个ssl证书&#xff0c;具体看这里 https://doc.crmeb.com/mer/mer2/4257 二、完成第一步后&#…

云动态摘要 2024-06-20

给您带来云厂商的最新动态,最新产品资讯和最新优惠更新。 最新优惠与活动 [低至1折]腾讯混元大模型产品特惠 腾讯云 2024-06-06 腾讯混元大模型产品特惠,新用户1折起! 云服务器ECS试用产品续用 阿里云 2024-04-14 云服务器ECS试用产品续用 最新产品更新 [功能优化]费用…

栈,队列,数组,链表

一.数据结构概述 数据结构是计算机底层存储,组织数据的方式。 是指数据相互之间是以什么方式排列在一起的。 数据结构是为了更加方便的管理和使用数据,需要结合具体的业务场景来进行选择。 一般情况下,精心选择的数据结构可以来更高的运行或者存储效率。 二.学好数据结构三大…

AI绘画ComfyUI-插件-面部修复,快速入门安装使用!

这期给大家分享一个插件AI绘画 ComfyUI的——Impact Pack ComfyUI也是隶属于Stable Diffusion的工作流形式的AI绘画工具。 这是一个综合节点&#xff0c;这期先介绍下这个插件中的面部修复功能 Impact Pack插件 1、下载插件 在ComfyUI管理器中安装节点&#xff0c;搜索Imp…

探索磁力搜索引擎:互联网资源获取的新视角

在当今数字化社会中&#xff0c;寻找和获取网络资源变得更加便捷和多样化。磁力搜索引擎作为这一趋势的一部分&#xff0c;提供了一种新颖而有效的方法来定位和获取用户所需的文件、媒体和其他数字内容。本文将深入探讨磁力搜索引擎的工作原理、使用场景及其在网络文化中的影响…

0.6 复合视频信号(CVBS)简介

0.6 复合视频信号简介 复合视频信号指在一个信号中包含了亮度信号、色度信号与同步信号&#xff08;包括场同步、行同步信号及行场消隐信号&#xff09;的视频信号。又可称为 CVBS&#xff0c;CVBS 为 Color&#xff0c;Video&#xff0c;Blanking&#xff0c;Sync 或者 compo…

酷得智能 电子方案开发 单片机方案定制

手持小风扇电子方案开发&#xff0c;东莞酷得智能科技有限公司为您服务 东莞市酷得智能科技有限公司 在消费类电子市场深耕多年&#xff0c;依托于市场团队对市场的敏锐度、工程团队的技术积累、结合自身的创新以及和上游原厂深度合作&#xff0c;在2.4G通信、BLE、语音、马达…

java实现一个LRU缓存算法。

//LRU&#xff08;Least Recently Used&#xff09;缓存算法是一种常见的缓存淘汰策略&#xff0c; // 它的基本思想是保留最近被访问过的数据&#xff0c;淘汰最久未被访问的数据。下面是一个使用Java实现的简单LRU缓存算法&#xff1a; import java.util.LinkedHashMap; impo…

【Unity】AssetBundle打包策略

【Unity】AssetBundle打包策略 在游戏开发过程中&#xff0c;AssetBundle(AB)打包策略的重要性不容忽视。游戏开发者往往手动设置游戏资源包名进行管理&#xff0c;难免会造成资源确实或导致冗余&#xff0c;因此对于AB包的打包流程来说&#xff0c;进行策略管理显得十分重要。…

正定矩阵(Positive Definite Matrix)

正定矩阵&#xff08;Positive Definite Matrix&#xff09; flyfish Positive&#xff08;正数&#xff09; &#xff1a;在数学和统计学中&#xff0c;通常指大于零的数。在矩阵理论中&#xff0c;一个矩阵被称为正定&#xff0c;是因为它的性质类似于正数的性质。 Defini…

聚合大模型场景助力产业升级,WAIC 2024 容联云论坛即将开幕

前 言 Introduction 2024世界人工智能大会暨人工智能全球治理高级别会议&#xff08;简称“WAIC 2024”&#xff09;即将拉开帷幕&#xff0c;在世界人工智能大会组委会办公室的指导下&#xff0c;容联云将于7月6日主办容联云生成式应用与大模型商业化实践论坛。本次论坛还将获…

Bureau of Contacts延迟高、卡顿、无法联机怎么办?

Bureau of Contacts是一款最多支持四个人联机玩的恐怖游戏&#xff0c;由MIROWIN开发并发行&#xff0c;6月20日在steam推出抢先体验版&#xff0c;相信喜欢恐怖游戏的玩家已经等不及了。玩家会扮演一名特工&#xff0c;接触并调查超自然现象&#xff0c;游戏分为调查和驱魔两个…

C语言练习02-数组

一、求最值 已知数组元素为{33,5,22,44,55}&#xff0c;找出数组中的最大值并打印在控制台 #include<stdio.h> #include<math.h>int main() {int arr[] {33,5,22,44,55};int max arr[0];int len sizeof(arr) / sizeof(int); //注意&#xff1a;sizeof(arr)计算…

新世纪助力无锡市第二人民医院通过ITSS认证

通过江苏新世纪信息科技有限公司的咨询辅导&#xff0c;无锡市第二人民医院通过合规性审查、复核、评定审核环节&#xff0c;顺利完成ITSS通用要求的认证。近日&#xff0c;评定结果在“ITSS中国电子工业标准化技术协会信息技术服务分会”网站&#xff08;https://www.itss.cn&…

【0基础学爬虫】爬虫基础之自动化工具 Appium 的使用

大数据时代&#xff0c;各行各业对数据采集的需求日益增多&#xff0c;网络爬虫的运用也更为广泛&#xff0c;越来越多的人开始学习网络爬虫这项技术&#xff0c;K哥爬虫此前已经推出不少爬虫进阶、逆向相关文章&#xff0c;为实现从易到难全方位覆盖&#xff0c;特设【0基础学…

推荐系统三十六式学习笔记:原理篇.矩阵分解12|如果关注排序效果,那么这个模型可以帮到你

目录 矩阵分解的不足贝叶斯个性化排序AUC构造样本目标函数训练方法 总结 矩阵分解在推荐系统中的地位非常崇高。它既有协同过滤的血统&#xff0c;又有机器学习的基因&#xff0c;可以说是非常优秀了&#xff1b;但即便如此&#xff0c;传统的矩阵分解无论是在处理显式反馈&…

图解Sieve of Eratosthenes(埃拉托斯特尼筛法)算法求解素数个数

1.素数的定义 素数又称质数。质数是指在大于1的自然数中&#xff0c;除了1和它本身以外不再有其他因数的自然数。一个大于1的自然数&#xff0c;除了1和它自身外&#xff0c;不能被其他自然数整除的数叫做质数&#xff1b;否则称为合数&#xff08;规定1既不是质数也不是合数&…

(day1)数据类型详解及DML语句入门

一、数据类型 1、整型类型 &#xff08;1&#xff09;创建数据库 CREATE DATABASE ql_linux&#xff1b; CREATE SCHEMA IF NOT EXISTS ql_linux&#xff1b; //IF NOT EXISTS如果没有表就创建 SHOW DATABASE; //查看数据库 &#xff08;2&#xff09;创建表 C…