引言:
前面我们介绍了二叉树的链式存储结构,还有前中后序的遍历思想和分治算法的思想,那么这篇博客我们就来看看几道和二叉树有关的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.多多练习和总结。
这就是本文的大致内容,如有不足和错误之处还望能够指出,希望能和大家共同进步。
.