二叉树(二)---->链式二叉树(下)(二叉树的OJ题)

引言:
  前面我们介绍了二叉树的链式存储结构,还有前中后序的遍历思想和分治算法的思想,那么这篇博客我们就来看看几道和二叉树有关的OJ题目。
正文:
1.单值二叉树
2.检查两棵树是否相同
3.二叉树的前序遍历
4.对称二叉树
5.另一颗树的子树
6.从前序遍历构建二叉树
7.判断是否是完全二叉树

1.单值二叉树: 这是题目链接

在这里插入图片描述
  所谓的单值二叉树,指的就是是不是所有的节点存储的值都是一个数字,比如这棵树就是一课单值二叉树。那么我们应该怎么去做这道题呢?
我们不妨分如下几种情况去分析:

1.假设是空树,没有违背单值二叉树的定义,返回true
2如果不是空树,并且当前节点有左右孩子,判断和左右孩子的值是否相同,不同返回false,如果相同,递归左子树判断是否左子树单值,递归右子树判断是否单值

为了方便递归,我们写一个辅助的子函数来替代原来的函数进行递归,通常我们都是采取在原函数名前加下划线来代表子函数

bool _isUnivalTree(struct TreeNode* root)
{ //根节点为空或者是递归到空节点,返回true
    if(root==NULL)
     return true;

     //判断根节点和左孩子的值是否相同
    if(root->left && root->val !=root->left->val)
    return false;

     //判断根节点和右孩子的值是否相同
     if(root->right && root->val !=root->right->val)
    return false;

    //递归判断左子树和右子树
    return _isUnivalTree(root->left) && _isUnivalTree(root->right);
}

bool isUnivalTree(struct TreeNode* root){
 return _isUnivalTree(root);
}

给出对应的示例图:
在这里插入图片描述
由于递归展开图太庞大,所以我这里只画出右子树的递归展开图:
在这里插入图片描述
还是那句话,如果实在难以理解递归的原理,只有一种方式—>画出递归展开图并且结合调试来理解递归。
怎么样?递归是不是看起来代码很少,但是实际理解起来是十分困难的一件事情?接下来还有更复杂的题目等着呢?接下来的题目,如果难以理解,请自己动手画一下递归展开图,我也会画一部分的递归展开图(全画出来的画篇幅太大不好观察)

检查两棵树是否相同:题目链接
给定两棵二叉树,要求判断这两棵二叉树是否相同:

这是题目的具体的描述和测试用例:
在这里插入图片描述

这个题目的思路也是很简单
1.如果两棵树都是NULL,那么返回true
2.如果只有一棵空树,那么不可能两棵树是相同的树,返回false
3.如果不是前面两种情况,假设当前根节点指向的值不一样,这两颗一定不是相同的树,返回false,否则就递归判断左子树是否相同,右子树是否相同即可。

具体的实现代码如下:

bool isSameTree(struct TreeNode* p, struct TreeNode* q){
 //都是空树或者是都递归走到了空,返回真
  if(p==NULL && q==NULL)
  {
      return true;
  }
  
  //其中一个是空,另外一个不是空,返回假
  if(p==NULL || q==NULL)
     return false;

 //如果节点值不相同,那么直接返回假,如果是真,需要继续比较
  if(p->val !=q->val)
    return false;

  //递归左子树和右子树  
     return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}

递归写起来很舒服,但是理解起来就很困难了,我们同样画出递归展开图来帮助分析和理解:
测试样例如下:
在这里插入图片描述
相应的递归展开图

3.二叉树的前序遍历:
看到这道题目,你会觉得这道题不是上一篇博客讲过了,为什么还要特意再讲一次呢?确实我们在上一篇博客里讲了前序遍历,但这道题目和我们上篇博客里讲的思想是相同的,不过人家的要求不太一样,废话不多说,直接上题目:

二叉树的前序遍历:前序遍历升级版

具体的题目的信息如下:
在这里插入图片描述

先前我们是用打印的方式遍历,但是这道题目要求我们把前序遍历的结果放到一个数组里面去,并且返回这个数组的地址
或许你也没有觉得多难,直接上手开始创建一个数组。
带来的后果就是从一开始整个方向就错了,这里我们不能返回局部数组!

原因一:我们不知道数组要开辟多大
原因二:局部的临时数组在生命周期一就被释放了,里面的内容变成随机值无法访问

所以这种需要返回地址的题目我们都需要malloc,题目里也给了一定的提示:

The returned array must be malloced, assume caller calls free().//意思就是返回的数组必须是malloc的,函数的调用者会帮助你释放

不过到了这里,即使我们已经知道数组要malloc,但是我们需要malloc多大的空间又是一个复杂的问题?这也是C语言不足的一个地方,所以在动态申请数组之前我们还需要求一下节点的个数
所以我们写出如下的题解代码:

 //求树的节点个数
int TreeSize(struct TreeNode* root)
{
    return root==NULL ? 0:TreeSize(root->left)+TreeSize(root->right)+1;
}
//用于递归的子函数
void _preorder(struct TreeNode* root,int* a,int i)
{
    //前序的思想
   if(root==NULL)
     return;
     a[i++]=root->val;
     _preorder(root->left,a,i);
     _preorder(root->right,a,i);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize){
    int i=0;
    int size=TreeSize(root);
    int* ans=(int*)malloc(sizeof(int)*size);
    _preorder(root,ans,i);
    return ans;
}

这时候你自信满满地提交代码,结果却出乎意料:
在这里插入图片描述
奇怪,为什么没有输出结果呢?是因为你的代码和后台的代码合并以后,后代的代码不知道需要访问多大的空间导致的!或许你主要到了这里还有一个returnSize参数,这个参数是一个输出型参数,作用是告知外界需要访问多大的空间要注意,这里的returnSize使用指针的原因是为了能够改变外界的访问参数(C语言通过形参改变实参只能通过传址改变
改进代码如下:

//求树的节点个数
int TreeSize(struct TreeNode* root)
{
    return root==NULL ? 0:TreeSize(root->left)+TreeSize(root->right)+1;
}
//用于递归的子函数
void _preorder(struct TreeNode* root,int* a,int i)
{
    //前序的思想
   if(root==NULL)
     return;
     a[i++]=root->val;
     _preorder(root->left,a,i);
     _preorder(root->right,a,i);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize){
    int i=0;
    int size=TreeSize(root);
    int* ans=(int*)malloc(sizeof(int)*size);
    //输出型参数
    *returnSize=size;
    _preorder(root,ans,i);
    return ans;
}

这次改进了代码以后,我们进行测试,结果如下:
在这里插入图片描述
奇怪,为什么这里进来一个随机值呢?这就和递归有关系了!
函数递归每次都会创建栈帧,每个栈帧之间的i都是独立的,并且是上一层栈帧中i的拷贝,一旦函数栈帧释放,返回上一级栈帧,i并没有被累加!
但是我们需要的是每次放入元素以后需要对下标++,基于这里我们就必须传递下标的地址!所以最终的AC代码如下:

//求树的节点个数
int TreeSize(struct TreeNode* root)
{
    return root==NULL ? 0:TreeSize(root->left)+TreeSize(root->right)+1;
}
//用于递归的子函数
void _preorder(struct TreeNode* root,int* a,int* pi)
{
    //前序的思想
   if(root==NULL)
     return;
     //*号优先级低,要先括号起来
     a[(*pi)++]=root->val;
     _preorder(root->left,a,pi);
     _preorder(root->right,a,pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize){
    int i=0;
    int size=TreeSize(root);
    int* ans=(int*)malloc(sizeof(int)*size);
    //输出型参数
    *returnSize=size;
    _preorder(root,ans,&i);
    return ans;
}

看似很简单的问题,但是有着诸多的细节,稍有不慎代码就通过不了。
4.对称二叉树

对称二叉树: 从这里跳转

题目具体要求如下:
在这里插入图片描述
那么这道题的具体思路如下:

1.如果当前的树是空树,返回true
2.如果不是空树,判断左子树是否和右子树对称.

  这里我们就需要使用一个辅助的子函数来处理子树是否对称的逻辑:

1.如果两颗子树为空,返回true
2.而如果其中一颗树为空,另外一棵树非空,那么这两颗树一定不对称,返回false
3.不满足1,2就递归走起来

所以我们写出如下的代码:

bool _isSymmetricTree(struct TreeNode* proot,struct TreeNode* qroot)
{   //左右子树都是空为真
    if(proot==NULL && qroot==NULL)
       return true;
       //二者至少一个非空,如果一个为空,返回假
    if(proot==NULL || qroot==NULL)
       return false;
       return proot->val == qroot->val 
            && _isSymmetricTree(proot->left,qroot->right)
            && _isSymmetricTree(proot->right,qroot->left);
}

bool isSymmetric(struct TreeNode* root){
   if(root==NULL)
    return true;
    //辅助子函数调用递归
    return _isSymmetricTree(root->left,root->right);
}

这里我们以示例一来展开分析:
在这里插入图片描述
对应的递归展开图如下:
在这里插入图片描述
其实这道题目就是相同的二叉树的变形,只不过这里变成了判断一棵左子树和另一颗右子树相同,本质上是没有太大区别的!

另一颗树的子树:
  这道题我特意加粗并且还用了红字,说明接下来这道题目十分难啃,因为这道题目理解起来非常的困难,因为作者自己在做这道题的时候也是花费了好久时间,画了好久的递归展开图才理解了这道题。

另一棵树的子树:题目传送门
题目的描述如下:
在这里插入图片描述
那么这道题目要求判断另一棵树是否是当前树的子树,题目说自身也可以是做是自己的子树,就意味着我们需要使用isSameTree这个函数接口!所以我们不妨从这个函数着手分析:
1.如果当前的树和给定的树相同,返回true.
2.不满足条件1,说明一定不是相同的树,递归判断是不是左子树的子树和右子树的子树,如果是其中一棵子树的子树,那么就不需要再判断另外一棵了,所以这里的逻辑关系是或!

解题代码如下:

bool isSameTree(struct TreeNode* p, struct TreeNode* q){
 //都是空树或者是都递归走到了空,返回真
 if(p==NULL && q==NULL)
  {
      return true;
  }

  //其中一个是空,另外一个不是空,返回假
  if(p==NULL || q==NULL)
     return false;

 //如果节点值不相同,那么直接返回假,如果是真,需要继续比较
 if(p->val !=q->val)
    return false;
  //递归左子树和右子树  
     return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
//如果root和subroot不一样,那么就要递归调用左子树和右子树(判断是不是相同)
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
   
  if(root==NULL)
     return false;
   return isSameTree(root,subRoot)
        || isSubtree(root->left,subRoot)
        || isSubtree(root->right,subRoot);
}

我做这道题的时候,最难以理解的就是就是这段代码:

if(root==NULL)
  return false;

那么针对这个问题,给出这么一个如下的测试用例图来分析就可以找出答案:
在这里插入图片描述
我们画出如下的递归展开图:
在这里插入图片描述
如图,当测试用例是这样的情况的时候,递归到某一层会出现对空指针的解引用,所以我们要处理root为NULL的情况!另外,假设isSameTree返回true,那么必然不会出现空指针解引用的情况,所以出现空指针解引用一定是root为空,subroot非空的情况,所以当root为空的时候我们return false
4.从前序遍历构建二叉树(较难)

题目传送门

题目的具体信息如下:
在这里插入图片描述
  那么这道题目给定一个二叉树的前序序列要求我们构建一棵二叉树,一般来说构建一棵二叉树需要前序和中序遍历,或者是后序和中序遍历序列才能构建,不过这道题的特殊之处在于它把空的节点表示出来,所以即使只给一个前序的序列也是能够构建这棵树的.

abc##de#g##f###
利用前序的思想:根,左子树,右子树构建如下:

在这里插入图片描述
接下来我们写出代码,和LeetCode不同,牛客网的这道题目是IO型,也就是输入和输出需要自己写,也就是完整地写一个程序,这是我们未来面试都必须经历的一关。

#include<stdio.h>
#include<stdlib.h>
typedef struct BinaryTreeNode
{
    char val;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
}BTNode;
//前序思想构造二茶树
BTNode* Create(char* ch,int* pi)
{
    if('#'==ch[*pi])
    {
        (*pi)++;
        return NULL;
    }
    BTNode* root=(BTNode*)malloc(sizeof(BTNode));
    if(NULL==root)
    {
        printf("malloc fail\n");
        exit(-1);
    }
    root->val=ch[*pi];
    ++(*pi);
    root->left=Create(ch,pi);
    root->right=Create(ch,pi);
    return root;
}
void InOrder(BTNode* root)
{
    if(root==NULL)
        return;
    InOrder(root->left);
    printf("%c ",root->val);
    InOrder(root->right);
}
int main()
{  
    char ch[100]={0};
    scanf("%s",ch);
    int i=0;
    BTNode* root=Create(ch,&i);
    InOrder(root);
    return 0;
}

判断是否是完全二叉树:
曾经有一道面试题考过判断一棵树是否是完全二叉树,那么这道题目利用的就是层序遍历的思想,我们先前的层序遍历是如果非空我们就入队,左右孩子非空才出队列,但这里不同,无论是否是空,我们都要入队列,其他操作和之前类似。当队头节点为空的时候,我们不再往里入节点,接下来把里面的节点依次出来。

如果取到非空,就不是完全二叉树反之就是完全二叉树

具体的实现代码如下:

// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		QueuePush(&q, root->left);
		QueuePush(&q, front->right);
		//出到空,结束
		if (front == NULL)
			break;
	}
	//处理逻辑
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		if (front)
		{  //不是完全二叉树
			QueueDestroy(&q);
			return false;
		}
	}
	//走到这里就是完全二叉树
	return true;
}

  总结:
  1.对于一棵二叉树,我们要有意识地看成左子树和右子树还有根,用分治算法去处理
  2.难以理解递归地时候画一画递归展开图,这是理解递归最好地武器!
  3.多多练习和总结。

这就是本文的大致内容,如有不足和错误之处还望能够指出,希望能和大家共同进步。
.

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值