【数据结构】二叉树

一、二叉树链式结构实现:

  1. 链式二叉树的节点结构:

typedef char BTDatatype;
typedef struct BinaryTreeNode
{
    BTDatatype data;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
}BTNode;

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二叉树结构掌握还不够深入,为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。

再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:

1.空树

2.非空:根节点,根节点的左子树、根节点的右子树组成的。

代码实现:

BTNode* BuyNode(BTDatatype x)
{
    BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
    if (newnode == NULL)
    {
        perror("mallco fail");
        exit(-1);
    }
    newnode->data = x;
    newnode->left = NULL;
    newnode->right = NULL;
}
BTNode* n1 = BuyNode(1);
    BTNode* n2 = BuyNode(2);
    BTNode* n3 = BuyNode(3);
    BTNode* n4 = BuyNode(4);
    BTNode* n5 = BuyNode(5);
    BTNode* n6 = BuyNode(6);


    n1->left = n2;
    n1->right = n4;
    n2->left = n3;
    n4->left = n5;
    n4->right = n6;

从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。

  1. 二叉树的遍历:

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历

  • 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。

  • 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。

  • 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

2.1前序遍历:

前序遍历:即按照:根->左子树->右子树 的顺序访问。

意为访问一条路上所有的根,再访问所有的左子树,最后访问所有的右子树。

按照上图这棵树,前序遍历访问顺序为:

1、2、3、NULL、NULL 、NULL、4、5、NULL 、NULL、6、NULL、NULL

函数栈帧展开图为:

代码实现:

void PrevOrder(BTNode* root)
{
    if (root == NULL)
    {
        printf("NULL ");
        return;
    }
    //先访问再递归
    printf("%d ", root->data);//先访问节点打印
    PrevOrder(root->left);//左子树遍历
    PrevOrder(root->right);//右子树遍历
}
  • 为方便理解遍历过程,代码中将叶子节点的子节点用NULL表示出来了,正常遍历是没有NULL的。

  • 仔细观察代码实现,递归到底是怎么实现的?

递归的究极思路在于将大问题分治,转化为小问题,就以上图为例,我们要对整个树进行遍历,那么我跟可以将这一整棵树分为几个小树:

而这棵树又能分为如下图的两棵小树:

我们对上边这棵小树进行详细遍历分解,可以看出它的左子树为3右子树为NULL,根据代码实现,先访问并打印2,然后递归到其左子树(3),访问并打印,并继续递归到其左子树,但3的左子树为NULL,当root== NULL时,返回,然后程序进行到访问3的右子树,同样也是NULL,返回,3的子树遍历完毕,程序来到2的右子树遍历,2的右子树为NULL,同样返回。

  • 理解以上过程,我们可以看出递归是不会一直进行下去的,程序当中必然有其截止条件,当遇到截止条件时,程序将返回,直到整个程序递归完成。

理解递归的最好办法就是自己画出函数栈帧的展开图。

2.2中序遍历:

中序遍历:即按照: 左子树->根->右子树 的顺序访问。

按照上图这棵树,中序遍历访问顺序为:

NULL 3 NULL 2 NULL 1 NULL 5 NULL 4 NULL 6 NULL

  • 中序遍历就是先访问并打印左子树节点,与前序相比就是访问顺序不同。

代码实现:

void InOrder(BTNode* root)
{
    if (root == NULL)
    {
        printf("NULL ");
        return;
    }
    InOrder(root->left);
    printf("%d ", root->data);
    InOrder(root->right);
}

2.3后序遍历:

后序遍历:即按照:左子树->右子树->根节点的顺序访问。

按照上图这棵树,后序遍历访问顺序为:

NULL NULL 3 NULL 2 NULL NULL 5 NULL NULL 6 4 1

代码实现:

void PostOrder(BTNode* root)
{
    if (root == NULL)
    {
        printf("NULL ");
        return;
    }
    PostOrder(root->left);
    PostOrder(root->right);
    printf("%d ", root->data);
}

2.4层序遍历:

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

  • 那么如何实现层序遍历?

我们采用队列来实现层序遍历,首先当树不为空树时,我们将跟根节点入队,注意!!这里我们将根节点入队,而不是根节点的值,那么为什么将根节点入队?如何入队?如果将入队的是根节点的值,那么我们将无法找到其子树,无法继续遍历,将整个根节点入队操作起来复杂,所以我们放入队列中的是节点的指针,所以队列的类型就会编程BTNode*类型,根据队列的特性(先进先出),将根节点出队,同时将其左右子树中不为空入队,循环往复,直到遍历完整棵树,下面我们操作一下:

就根据上图这棵树操作:

如果树不为空,将1入队,然后将1出队打印出来,同时将2、4入队,将2、4出队打印,同时将3、5、6入队,然后将3、5、6出队打印,完成遍历。

代码实现:

void LevelOrder(BTNode* root)
{
    Queue q;
    QueueInit(&q);
    //树不为空,将第一个入队
    if (root)
    {
        QueuePush(&q, root);
    }
    while (!QueueEmpty(&q))
    {
        //出队内元素,然后将其子树入队
        BTNode* front = QueueHead(&q);
        printf("%d ", front->data);
        QueuePop(&q);
        //左子树不为空,入队
        if (front->left)
        {
            QueuePush(&q, front->left);
        }
        if (front->right)
        {
            QueuePush(&q, front->right);
        }
    }
    printf("\n");
    QueueDestory(&q);
}
  1. 二叉树节点个数及高度等

3.1二叉树的节点个数:

对于求解二叉树的节点个数,固然也是采用递归思想来解决,将问题逐级拆分,整棵树的节点 = 左子树节点 + 右子树节点 + 根节点。

现在我们拆分一个小树来求解一下节点个数:

  • 上图这颗小树节点个数 = 4的左节点数+4的右节点数+1,即为5的节点个数 + 6的节点个数+1,5的节点个数 = 5的左节点个数+5的右节点个数 + 1 = 0+0+1;6和5同为叶子节点,同样节点个数=0+0+1,那么这棵小树的节点个数 = 1+1+1 = 3,然后这棵小树会作为一个节点的右子树节点个数去计算整个树的节点个数。

代码实现:

int BTreeSize(BTNode* root)
{
    //分治管理 -- 整棵树 = 左子树 + 右子树 + 1(树顶节点)
    return root == NULL ? 0 :
        BTreeSize(root->left) + BTreeSize(root->right) + 1;
}

函数栈帧展开图:

3.2二叉树叶子节点个数:

什么节点是叶子节点?叶子节点是没有子树的节点,那么怎么求叶子节点的个数呢?我们采用分治思想,举个例子:假如学校要统计学生个数,1个校长将任务分配给2个院长,两个院长将任务分给4个主任,主任逐层向下分给各班主任,班主任让班长统计人数并逐层上报回上一级,叶子节点就相当于每个学生,所以遇到"班主任"、"院长"不需要+1,遇到"学生"+1即可。

代码实现:

int BTreeLeafSize(BTNode* root)
{
    if (root == NULL)
    {
        return 0;
    }
    if (root->left == NULL &&
        root->right == NULL)
    {
        return 1;
    }
    return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}

注意!!可以的话自己画一下函数栈帧的展开图,加深理解二叉树递归。

3.3二叉树第k层节点个数:

求第k层的节点个数,看起来与求叶子节点个数相似,但实际上确实如此,就是判断节点的条件改变,那么我们又如何判断在第k层的节点呢?

就如上图所示,k从递归开始每递归一层减一,当k=1时,就是目标层数。

代码实现:

int BTreeLevelKSize(BTNode* root,int k)
{
    //根据当前的递归层与目标层差数,判断那层是第k层
    if (root == NULL)
    {
        return 0;
    }
    if (k == 1)
    {
        return 1;
    }
    return BTreeLevelKSize(root->left, k - 1) + BTreeLevelKSize(root->right, k - 1);
}

3.4二叉树高度:

求二叉树的高度,一个节点就算一个高度,但二叉树分为左右子树,而二叉树的高度是取最高子树的高度,所以需要对比左右子树的高度并取其中较大的,再加上根节点的高度。

代码实现:

int BTreeHeight(BTNode* root)
{
    if (root == NULL)
    {
        return 0;
    }
    int leftHeight = BTreeHeight(root->left);
    int rightHeight = BTreeHeight(root->right);
    return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

3.5二叉树查找值为x的节点:

按照值来查找节点,同样得思路,先在左树查找,找不到去右树找,在查找时,是x节点返回节点的地址,然后再判断返回的指针是否为空。

代码实现:

BTNode* BTreeFind(BTNode* root, BTDatatype x)
{
    if (root == NULL)
    {
        return NULL;
    }
    if (root->data == x)
    {
        return root;
    }

    BTNode* ret1 = BTreeFind(root->left, x);
    if (ret1)
        return ret1;
    BTNode* ret2 = BTreeFind(root->right, x);
    if (ret2);
        return ret2;
    return NULL;
}

函数栈帧展开图:

3.6判断是否为完全二叉树:

如何判断一棵树是否是完全二叉树?满二叉树根据其高度和节点个数就可以判断出,而完全二叉树节点个数不确定,无法从这个方向判断,但完全二叉树有一个特点,就是节点是连续的,我们可以根据这个方向解决问题,将完全二叉树层序遍历,遇到NULL就停止遍历,再检查队列中是否有非NULL,有则不是完全二叉树。

代码实现:

bool BTreeComplete(BTNode* root)
{
    Queue q;
    QueueInit(&q);
    if (root)
    {
        QueuePush(&q, root);
    }
    
    while (!QueueEmpty(&q))
    {
        BTNode* front = QueueHead(&q);
        QueuePop(&q);
        //遇到空跳出判断
        if (front == NULL)
        {
            break;
        }
        else
        {
            QueuePush(&q, front->left);
            QueuePush(&q, front->right);
        }
    }

    //判断 - 后面全是空即为完全二叉树
    while (!QueueEmpty(&q))
    {
        BTNode* front = QueueHead(&q);
        QueuePop(&q);
        if (front != NULL)
        {
            QueueDestory(&q);
            return false;
        }
    }
    QueueDestory(&q);
    return true;
}

3.7二叉树销毁:

顾名思义,二叉树的销毁必然也是递归实现的,那么我们根据什么顺序来销毁二叉树呢?如果用前序,那么在销毁一个节点后,我们将无法找到其子树节点,可以看出,后序遍历可以很好的解决这个问题,先销毁节点的左右节点然后再销毁改节点。

代码实现:

void BTreeDestory(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    BTreeDestory(root->left);
    BTreeDestory(root->right);
    free(root);
}

3.8根据二叉树的遍历顺序来构建二叉树:

还是上面这棵树,我用0来表示空树,它的前序遍历数组为:1 2 3 0 0 0 4 5 0 0 6 0 0

代码实现:

BTNode* BTreeCreate(BTDatatype* a, int* pi)
{
    if (a[(*pi)] == 0)
    {
        (*pi)++;
        return NULL;
    }
    
    BTNode* root = (BTNode*)malloc(sizeof(BTNode));
    if (root == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    root->data = a[(*pi)++];
    root->left = BTreeCreate(a, pi);
    root->right = BTreeCreate(a, pi);
    //大同小异,中序、后序方法不变--即改动以上三条代码的顺序
    return root;
}
  1. 总结:

对于二叉树的学习,最主要的点在于理解递归的过程,可以多画程序的函数栈帧展开图来进一步理解二叉树的递归。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值