递归心决:(xdm好好感悟)
1.确定递归的结束条件
2.确定递归的单层逻辑
3.确定递归的参数和返回值
文章目录
- 一、链式二叉树接口
- 1.二叉树的结构体
- 2.手动造一棵二叉树
- 3.二叉树前、中、后序遍历(递归的神圣大门开启)
- 4.二叉树的结点个数
- 5.二叉树的叶子结点个数
- 6.二叉树的高度
- 7.查找二叉树结点值为x的结点
- 8.求二叉树第k层的结点总个数(这谁能想到?)
- 9.二叉树销毁
- 10.二叉树的层序遍历+判断二叉树是否为完全二叉树(终于不是递归了,谢天谢地)
- 二、二叉树相关OJ题
- 1.单值二叉树
- 2.二叉树的最大深度
- 3.相同的树
- 4.另一棵树的子树(套用相同的树接口)
- 5.二叉树遍历(注意下标 i 的参数设计)
- 6.翻转二叉树
- 7.对称二叉树(学会自己构建子函数)
- 8.二叉树的前序遍历(学会自己构建子函数)
- 9.平衡二叉树
- 三、有关递归的心得
一、链式二叉树接口
1.二叉树的结构体
这篇文章我们就以下图举例来进行接口的实现。
另外说一点哈,我们马上就要进入递归的神圣殿堂了,以后看待二叉树就不能和以前那样看待了,那怎么看待呢?就以下面图那样去看待,每个度小于2的结点是有NULL的,所以你必须看到这些NULL。
我们现在实现的是链式结构,不是之前的顺序结构,所以在结构体定义这里每个结点都需要两个指针,一个指向左树,一个指向右树。
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
2.手动造一棵二叉树
为方便后续关于二叉树的一些接口以及OJ题能够顺利进行,我们这里手动造一棵二叉树出来
BTNode* BuyBTNode(BTDataType x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
newnode->left = newnode->right = NULL;
newnode->data = x;
return newnode;
}
int main()
{
BTNode* n1 = BuyBTNode(1);
BTNode* n2 = BuyBTNode(2);
BTNode* n3 = BuyBTNode(3);
BTNode* n4 = BuyBTNode(4);
BTNode* n5 = BuyBTNode(5);
BTNode* n6 = BuyBTNode(6);
n1->left = n2;
n2->left = n3;
n1->right = n4;
n4->left = n5;
n4->right = n6;//这样我们就手动建立了一棵二叉树
return 0;
}
3.二叉树前、中、后序遍历(递归的神圣大门开启)
我们先确定一下递归的单层逻辑,逻辑很简单,如果是前序遍历,我们就输出根结点,左子树根节点,右子树根节点的值,以这样的逻辑方式递归遍历整个二叉树。
那递归到什么位置?我们不继续深层次递归了呢?开始返回上一层呢?也很简单,当我们遇到空结点时,就该归了。
void PrevOrder(BTNode* root)//根,左子树,右子树
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
void InOrder(BTNode* root)//左子树,根,右子树
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
void PostOrder(BTNode* root)//左子树,右子树,根
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
4.二叉树的结点个数
单层逻辑是什么呢?我们只要求出左子树结点个数,再加上右子树节点个数,最后再加1就得到二叉树的总结点个数了。
递归什么时候结束呢?遇到叶子节点说明二叉树已经递归遍历完了,该返回了。
int BinaryTreeSize(BTNode* root)//先得到左子树结点个数再获得右子树节点个数再加1,就得到整个二叉树的结点个数了
{
if (root == NULL)
return 0;
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
5.二叉树的叶子结点个数
我们可以先确定一下递归的单层逻辑,不断向下递归找叶子结点,然后把左右子树的叶子结点总个数加起来。
结束条件有两个,一个是遇到了叶子结点,另一个是遇到NULL结点,需要返回0.
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
6.二叉树的高度
大方向该如何思考呢?我们知道想要得出一个二叉树的高度,可以通过计算左右子树的最大高度然后再加1,这样就可以得出二叉树的高度了,左右子树根节点都为NULL也没关系,我们随便拿出一个高度+1,这样左右子树都为NULL的结点的高度就会是1了。
递归深度什么时候结束呢?遇到NULL时,我们递就应该结束,要开始归了。
int TreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);//把返回的结果记录下来
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
7.查找二叉树结点值为x的结点
查找这类型的接口,递归结束无非就两种情况,第一种找到节点,该结束了,第二种没有找到,都已经把整个二叉树找遍了还是没有找到。
所以像这一类型的接口,递归结束的条件还是比较显而易见的。
对于单层逻辑的话也是比较简单的,我们将问题拆为,只拿根节点中的data和x比较看是否相等,相等我们立马返回,不相等就拿左子树根节点的data和x比较,如果还不相等,我们就继续拿右子树根节点的data和x进行比较,就这样一直比,直到比到NULL结点,如果左子树和右子树都已经比到NULL结点了,还是没有找到对应的root,那就只能返回NULL了。
这也就是我们的单层逻辑
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)//都已经找到空结点的位置了还是没找到或者这就是一个空树,肯定不存在我要找的值
{
return NULL;
}
if (root->data == x)
{
return root;
}
return BinaryTreeFind(root->left, x)||
BinaryTreeFind(root->right, x);
}
8.求二叉树第k层的结点总个数(这谁能想到?)
这道题怎么说呢?真的挺难的,无论是单层逻辑,还是递归结束条件,都是很难想出来的。
先说一下递归的单层逻辑,这个逻辑其实是利用了递归层与层之间的相对位置关系。就是对于根节点来说的第K层结点是子树的第K-1层,以这样的方式来进行问题的分解,然后把左子树和右子树中第K层的结点个数加起来,就可得到第K层结点个数了。
对于递归结束条件,这个还好,没有像单层逻辑那样让人束手无策,遇到NULL返回的结点个数应该是0,当层数为1时,返回的结点个数应该是1。
最后再把
int BinaryTreeLevelKSize(BTNode* root, int k)//k层结点个数 = 左子树的k-1层结点个数 + 右子树的k-1层节点个数
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLevelKSize(root->left, k - 1) +
BinaryTreeLevelKSize(root->right, k - 1);
}
9.二叉树销毁
其实我们只要采用后序遍历进行结点的销毁就可以了,如果一开始就释放了根节点的话,很容易造成左右孩子结点无法找到,所以我们采用后续遍历的方法,来进行二叉树结点空间的释放。
当然,销毁的递归结束条件也是显而易见的,遇到NULL时,我们肯定也就不会进行空间的free了,要进行返回了。
void BinaryTreeDestory(BTNode** root)
{
if (root == NULL)
return;
BinaryTreeDestory((*root)->left);
BinaryTreeDestory((*root)->right);
free(*root);//将每一个根节点都置空了。
*root = NULL;
}
10.二叉树的层序遍历+判断二叉树是否为完全二叉树(终于不是递归了,谢天谢地)
做了这么多的递归题之后,感觉就没有比递归更难得题了,所以好不容易碰见一个利用队列数据结构来实现得接口,真的令人兴奋不已,只要不是递归,啥都好说。
具体代码的实现看下面就好了,我在这里说几个需要注意的点。
首先我们在队列中存储的是二叉树结点的地址,这样可以省很多的空间,我们最初把根节点地址入队列,然后下面每次出队列之后就要入队列左右子结点,前提是它不为空,只要不为空,我们就入队列,这个循环的结束条件就是当队列为空时,我们不再进行pop和push的操作。
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);//队列中存放的数据应该是二叉树的每个结点,但由于结点拷贝空间太大,我们存放每个结点的地址
if (root)
QueuePush(&q, root);//root不是空,将根节点指针入队列
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
}
对于完全二叉树的判断其实也是比较简单的,如果队列中的元素有NULL,并且NULL后面如果有不为空的元素那就足以说明此棵二叉树不是完全二叉树,如果后面的元素都是NULL,那就说明这个二叉树是完全二叉树。
与层序遍历稍有不同的是,我们入队列时会将所有的NULL也都入队列,利用NULL来判断是否为完全二叉树
如果树不是完全二叉树的话,记得也要将动态开辟的空间释放掉,防止内存泄露的发生。
bool TreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)//遇到NULL,就可以开始判断是否为完全二叉树了
{
break;
}
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
//出到NULL以后,如果后面全是空,则是完全二叉树,如果有非空结点,那就不是完全二叉树
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front != NULL)
{
QueueDestroy(&q);//防止内存泄露
return false;
}
}
QueueDestroy(&q);//如果别人写的是带有哨兵卫结点的队列的话,这里有destroy的话,也可以防止内存泄露
//即使是队列为空的时候,再删除也不会报错,因为队列为空,说明动态开辟的结点已经全部释放完毕了,head和tail也都为NULL了
//所以会直接从QueueDestroy中的while循环跳出来,不进行节点空间的释放。
return true;//return一不小心就内存泄漏了,因为如果不是完全二叉树的话,队列里还是有结点的空间是没有释放的,所以很容易造成内存泄露
}
二、二叉树相关OJ题
1.单值二叉树
单值二叉树
我们还是先来分析一下,递归的结束条件,当我们的root==NULL时,这并不能说明我们的二叉树不是单值二叉树,所以出现这样的情况时,我们递归也要结束返回true,如果子节点中某个结点不为空,那就拿这个子节点和父节点进行比较,不相等,递归也是要结束的。
以上就是我们的单层逻辑,对于左子树和右子树,我们的判断方式和父节点也是一样的。
bool isUnivalTree(struct TreeNode* root){
if(root==NULL)
return true;
if(root->left&&root->left->val!=root->val)//左边不等于空的时候,我们才可以和左进行比较
return false;
if(root->right&&root->right->val!=root->val)
return false;
return isUnivalTree(root->left)&&isUnivalTree(root->right);
}
2.二叉树的最大深度
二叉树最大深度
我们先来确定一下单层的递归逻辑,我们只要找出左右子树中最大的深度然后+1,就可以得到我们二叉树的最大深度了。
然后我们分析递归的结束条件。
1.当我们的root也就是根节点等于NULL时,其实能说明两种情况,第一种,这个树本来就是空树,第二种当我们遇到空结点时也说明递归要结束了
2.当我们遇到叶子节点时,递归也应该结束了,返回1的高度
3.当我们计算好左子树和右子树的高度之后,我们用一个三目运算符来进行二叉树深度的返回。
int maxDepth(struct TreeNode* root){
if(root==0)
return 0;
if(root->left==NULL&&root->right==NULL)
return 1;
int left=maxDepth(root->left);
int right=maxDepth(root->right);
return left>right?left+1:right+1;
}
3.相同的树
相同的树
这个题其实还是延续了我们之前的单层逻辑,我们先比较两个树的根结点,再比较两个树的左子树的根节点,然后比较两个树的右子树的根节点。这就是我们的单层逻辑
然后分析递归的结束条件,当两个节点值不相等,或一个结点为空,一个不为空,等我们的递归就要结束了。当然我们还不能漏下一个递归结束条件,这个也是我们容易忽略的,当两棵树都是NULL时,他们也是相同的,所以我们知道,当两棵树都是空时,我们的递归也要结束了。
由于这题是接口型的题,我们也就不用自己设计递归函数的参数和返回值了,这也为我们省去了不少的时间。
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
//1.每一层递归的逻辑,就是先比较根,再比较左子树的根节点,然后比较右子树的根节点
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);
//到了这里说明此时比较的两个结点值相同,我们继续比较左子树的根结点和右子树的根节点
//左和右都相等才能说明两个树相同
}
4.另一棵树的子树(套用相同的树接口)
另一棵树的子树
我们先来分析递归的结束条件,当我们在左边的树中,一直找到NULL了,那肯定说明左边的树中没有我们右边的树,还有一种情况就是当我们在左边的树中找到我的subRoot了,此时递归也就需要停止了。
我们的递归单层逻辑其实也是比较简单的,由于我们前面做过相同的树那道题,所以我们利用这个接口可以帮助我们来快速解决这个子树问题,我们拿root中的每一个结点都作为一次根节点,和我们的subRoot进行比较。
比较的顺序是先比较根结点,再比较左子树的根节点,在比较右子树的根节点。
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
//1.每一层递归的逻辑,就是先比较根,再比较左子树的根节点,然后比较右子树的根节点
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);
//到了这里说明此时比较的两个结点值相同,我们继续比较左子树的根结点和右子树的根节点
//左和右都相等才能说明两个树相同
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
//拿右边的树和左边的每一棵子树进行比较,如果和其中某一棵子树相等,就返回true
if(root==NULL)//根节点都为空了,说明肯定root没有subroot
return false;
if(isSameTree(root,subRoot))
return true;
return isSubtree(root->left,subRoot)||isSubtree(root->right,subRoot);
//你如果把这里的函数调用不小心改成上面的函数来进行调用的话,就相当于你只对root的两个子树
//查找了一下,剩余的子树根本没查找
}//默认subroot不为空
//递归结束的条件是什么呢?
//1.就是如果我从左边的树中找到我的右边的树了,那就递归结束了
//2.或者来root以及root的左和右子树中找了半天,到NULL了都还没找到,那递归也应该结束了
5.二叉树遍历(注意下标 i 的参数设计)
二叉树遍历
这道题其实最大的难点就是,我们如何通过前序遍历的方式来递归创建一棵二叉树,至于后面的中序遍历二叉树,输出二叉树内容,那就是个小配菜,所以我们把重心放在如何去先序遍历字符串然后递归式的建立一棵二叉树上。
然后我们现在考虑两个问题:
1.递归结束条件
2.单层递归逻辑
3.递归的参数和返回值
哎呀细细的思考一下,其实这个题难就难在递归结束条件上面了,遇到叶子节点要归,子树建立完毕也要归,哎呀这就很烦人嘛,向我们以前做的低级递归题,哪有两个返回值啊,一个return就可解决递归了,现在不一样了,世道变了,不做点高级递归,怎么拿高新offer啊?
还有一个点就是递归的参数和返回值,单层递归逻辑,题目已经要求死了,你也不用思考,就是先建立根,再建立左树,最后建立右树。这样理解起来其实还是有些抽象,应该是先建立根再建立左子树的根,再建立右子树的根。 我们前序遍历,中序遍历,后序遍历等用的都是这个单层逻辑,例如中序:左子树的根节点,根节点,右子树的根节点。例如后续:左子树的根节点,右子树的根节点,根节点。他们的结束条件也相同,遇到NULL,就开始返回,也就是归,本题中的NULL结点其实就是值为字符#的结点
递归的参数,肯定得有个接收数组的指针,然后还得有个数组的下标,如果我们不用数组下标,用解引用指针来访问主函数里面的字符数组的话,是会出大问题的,因为每一次函数调用都要建立函数栈帧,每个函数栈帧里的指针不是同一个,他们是临时为了满足需求建立出来的指针变量,所以我们必须要用一个整型变量i的指针来接收整型变量的地址,让我们每一个函数栈帧操作的下标是同一个下标
一定要注意,我们单层逻辑是建立根节点,再建立左子树根节点,在建立右子树根节点,我们动态开辟的每一个结点在各自函数栈帧里都是本层子树的根节点,所以我们动态开辟时,接收地址的变量我们命名为root,这就是神圣的递归
#include <stdio.h>
#include <stdlib.h>
struct TreeNode
{
char val;
struct TreeNode*left;
struct TreeNode*right;
};
struct TreeNode*rebulidTree(char*str,int* pi)//我们这里应该传i的地址,过去,如果不这么干,每个函数栈帧都有自己的i,i++的时候,+的不是同一个i
{
if(str[*pi]=='#')//必须是#,我们才往后++
{
(*pi)++;
return NULL;
}
struct TreeNode*root=(struct TreeNode*)malloc(sizeof(struct TreeNode));
root->val=str[(*pi)++];
root->left=rebulidTree(str, pi);
root->right=rebulidTree(str, pi);
return root;
}
void InOrder(struct TreeNode*root)
{
if(root==NULL)
return;
InOrder(root->left);
printf("%c ",root->val);
InOrder(root->right);
}
int main() {
char str[100];
scanf("%s",str);
int i=0;
struct TreeNode*root=rebulidTree(str,&i);//传i的地址的话,上面函数的每次调用操作的就是同一个i了
//如果不用指针,绝对会出问题
InOrder(root);
return 0;
}
//递归建立一棵二叉树
//递归中序遍历二叉树
//递归需要传参,我们需要额外定义函数
6.翻转二叉树
翻转二叉树
翻转二叉树还是比较简单的嘛,就喜欢虐菜,哈哈哈。
单层逻辑:将结点的左右子树进行交换,其实本质上改变的是根节点左右指针的指向
递归结束条件:当交换的根节点变成NULL时,就开始返回,当然返回肯定是什么都不干的,等回到最初的函数栈帧时,这个二叉树就已经被我们翻转过来了。
struct TreeNode* invertTree(struct TreeNode* root){
if(root==NULL)
return NULL;
struct TreeNode*tmp=root->left;
root->left=root->right;
root->right=tmp;
invertTree(root->left);
invertTree(root->right);
return root;
}
7.对称二叉树(学会自己构建子函数)
对称二叉树
想要解决这个问题,我们采取的思想就是以根节点的左右子树根节点为轴,拿其中的一个左子树和另外的右子树,拿右子树和另外的左子树比较,看是否相等。
关于镜像对称的树,我们反着进行比较,一直递归到NULL,再返回。
对于我们要实现的接口,他的参数如果不符合我们思路所想,单独一个接口无法完成题目要求的话,极大可能一个接口是不够的,所以我们会采用自己构建子函数的方式来帮助完成系统所给接口的实现
bool _isSymmetric(struct TreeNode*root1,struct TreeNode*root2)
{
if(root1==NULL&&root2==NULL)
return true;
if(root1==NULL||root2==NULL)
return false;
if(root1->val!=root2->val)
return false;
return _isSymmetric(root1->left,root2->right)&&
_isSymmetric(root1->right,root2->left);
}
bool isSymmetric(struct TreeNode* root){
return _isSymmetric(root->left,root->right);
}
8.二叉树的前序遍历(学会自己构建子函数)
二叉树的前序遍历
这个题怎么说呢?其实就是在前序遍历的基础上增加了一些处理上的细节。
比如说我们需要动态开辟一个数组,然后前序遍历二叉树,将二叉树结点的值存到动态开辟的数组里面。
从而衍生出来的一个问题就是数组开辟应该开辟多大的空间,所以我们这里还需要一个接口来计算二叉树的结点数量,根据结点数量去开辟相应大小的数组。
另外就是,开辟数组的大小需要你以参数的形式返回到主函数那里,想要同时修改两个以上的值,我们一般有两种方法,第一种就是返回一个结构体,结构体包含诸多的值,第二种就是给函数多增加一个地址型的参数,对外面调用函数的某个变量进行修改。
int TreeSize(struct TreeNode*root)
{
if(root==NULL)
return 0;
return 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 size=TreeSize(root);
*returnSize=size;
int*a=(int*)malloc(sizeof(int)*size);//开辟一个数组
int i=0;
preorder(root,a,&i);
return a;
}
9.平衡二叉树
平衡二叉树
递归结束条件:
1.二叉树为空树时,不用比较就是平衡二叉树
2.二叉树的左右子树高度差大于1时,递归结束,不为平衡二叉树
单层递归逻辑:
我们比较每一个根节点下的左右子树的高度差是否符合要求即可,利用了之前求树的做大高度的接口,以此来求两个子树的高度。
int TreeHeight(struct TreeNode* root)
{
if(root==NULL)
return 0;
if(root->left==NULL&&root->right==NULL)
return 1;
int left=TreeHeight(root->left);
int right=TreeHeight(root->right);
return left>right?left+1:right+1;
}
bool isBalanced(struct TreeNode* root){
if(root==NULL)
return true;
int left=TreeHeight(root->left);
int right=TreeHeight(root->right);
if(abs(left-right)>1)
return false;
return isBalanced(root->left)&&isBalanced(root->right);
}
三、有关递归的心得
如果你碰见了一个高级递归的题,那他肯定是和以下两个方面是脱不开关系的。
一:递归结束情况较多,且不好控制
二:递归的单层逻辑难以挖掘,拆分子问题较难。
如果想要解决递归,我说几个重要的解决方法:
1.如果遇到不会的题,多去画递归展开图,多感悟递归的逻辑
2.递归心得:参数返回值设计,递归结束条件,递归单层逻辑
3.多写代码,多感悟题目,多画图,反复的去琢磨思考。