leetcode【数据结构简介】《二叉树》卡片——运用递归解决问题

Authur Whywait 做一块努力吸收知识的海绵
想看博主的其他所有leetcode卡片学习笔记链接?传送门点这儿

注:标题中带🚩的为相关编程练习

在之前的文章中,我们已经介绍了如何利用递归求解树的遍历。 递归是解决树的相关问题最有效和最常用的方法之一。

树可以以递归的方式定义为一个节点(根节点),它包括一个值和一个指向其他节点指针的列表。

递归是树的特性之一。

因此,许多树问题可以通过递归的方式来解决。 对于每个递归层级,我们只能关注单个节点内的问题,并通过递归调用函数来解决其子节点问题。

通常,我们可以通过 “自顶向下”“自底向上”递归来解决树问题。

下面将介绍这两种方案。

“自顶向下”的解决方案

“自顶向下” 意味着在每个递归层级,我们将首先访问节点来计算一些值,并在递归调用函数时将这些值传递到子节点。

“自顶向下” 的解决方案可以被认为是一种前序遍历
(如果不理解为什么可以认为是一种前序遍历,下面的递归函数原理的3-5步就可以很清楚地相互印证了)

下面将展示自顶向下递归函数原理:

1. return specific value for null node
2. update the answer if needed                      // anwer <-- params
3. left_ans = top_down(root.left, left_params)      // left_params <-- root.val, params
4. right_ans = top_down(root.right, right_params)   // right_params <-- root.val, params
5. return the answer if needed                      // answer <-- left_ans, right_ans

下面将给出一个求二叉树最大深度的伪代码:

【思路】我们知道根节点的深度是1。 对于每个节点,如果我们知道某节点的深度,那我们将知道它子节点的深度。 因此,在调用递归函数的时候,将节点的深度传递为一个参数,那么所有的节点都知道它们自身的深度。 而对于叶节点,我们可以通过更新深度从而获取最终答案。

1. return if root is null
2. if root is a leaf node:
3.      answer = max(answer, depth)         // update the answer if needed
4. maximum_depth(root.left, depth + 1)      // call the function recursively for left child
5. maximum_depth(root.right, depth + 1)     // call the function recursively for right child

下面是求二叉树最大深度的C++的代码

int answer;		       // don't forget to initialize answer before call maximum_depth
void maximum_depth(TreeNode* root, int depth) {
    if (!root) {
        return;
    }
    if (!root->left && !root->right) {
        answer = max(answer, depth);
    }
    maximum_depth(root->left, depth + 1);
    maximum_depth(root->right, depth + 1);
}

“自底向上” 的解决方案

在每个递归层次上,我们首先对所有子节点递归地调用函数,然后根据返回值和根节点本身的值得到答案。

这个过程可以看作是后序遍历的一种。

下面将展示自底向上递归函数原理:

1. return specific value for null node
2. left_ans = bottom_up(root.left)          // call function recursively for left child
3. right_ans = bottom_up(root.right)        // call function recursively for right child
4. return answers                           // answer <-- left_ans, right_ans, root.val

我们继续讨论之前树的最大深度的问题,但是使用不同的思维方式。

对于树的单个节点,以节点自身为根的子树的最大深度x是多少?

如果我们知道一个根节点,以其左子节点为根的最大深度为l和以其右子节点为根的最大深度为r,我们是否可以回答前面的问题? 当然可以,我们可以选择它们之间的最大值,再加上1来获得根节点所在的子树的最大深度。 那就是 x = max(l,r)+ 1。

这意味着对于每一个节点来说,我们都可以在解决它子节点的问题之后得到答案。 因此,我们可以使用“自底向上“的方法。

下面是自底向上递归函数的伪代码:

1. return 0 if root is null                 // return 0 for null node
2. left_depth = maximum_depth(root.left)
3. right_depth = maximum_depth(root.right)
4. return max(left_depth, right_depth) + 1  // return depth of the subtree rooted at root

同样,下面是C++的代码以供参考:

int maximum_depth(TreeNode* root) {
	if (!root) {
		return 0;                                 // return 0 for null node
	}
	int left_depth = maximum_depth(root->left);
	int right_depth = maximum_depth(root->right);
	return max(left_depth, right_depth) + 1;	  // return depth of the subtree rooted at root
}

总结

了解递归并利用递归解决问题并不容易。

当遇到树问题时,请先思考一下两个问题:

  • 你能确定一些参数,从该节点自身解决出发寻找答案吗?
  • 你可以使用这些参数和节点本身的值来决定什么应该是传递给它子节点的参数吗?

如果答案都是肯定的,那么请尝试使用 “自顶向下” 的递归来解决此问题。

或者你可以这样思考:

  • 对于树中的任意一个节点,如果你知道它子节点的答案,你能计算出该节点的答案吗?

如果答案是肯定的,那么 “自底向上” 的递归可能是一个不错的解决方法。

二叉树的最大深度🚩

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

给定二叉树 [3,9,20,null,null,15,7],
返回它的最大深度3.

分析

此题上文中已经给出思路,所以不再赘述,直接上代码。

代码实现以及执行结果

int max=0;
void TopDown(struct TreeNode* root, int depth){
    if(!root) return;
    if(!root->left && !root->right) max = max>depth? max : depth;
    TopDown(root->left, depth+1);
    TopDown(root->right, depth+1);
}

int maxDepth(struct TreeNode* root){
    if(!root) return 0;
    int depth=1;
    max = 0;	//这里需要初始化
    TopDown(root,depth);
    return max;
}

在这里插入图片描述

Tips

如果存在全局变量,一定要初始化!!!

比如本程序中的max

优化

全局变量的存在,使得稍有疏忽便酿成一些注意不到的bug。

所以下面对上述程序进行优化,处理了这种自己给自己挖坑的问题。代码以及执行结果展示如下:

void TopDown(struct TreeNode* root, int depth, int* max){
    if(!root) return;
    if(!root->left && !root->right) (*max) = (*max)>depth? (*max) : depth;
    TopDown(root->left,depth+1, max);
    TopDown(root->right,depth+1, max);
}

int maxDepth(struct TreeNode* root){
    if(!root) return 0;
    int depth=1, *max;
    max = (int *)malloc(sizeof(int));
    *max = 0;
    TopDown(root, depth, max);
    return *max;
}

在这里插入图片描述

和之前的程序进行比较,传入函数的参数多了一些。

另外,值得注意的是,返回的是*max,而不是max。如果传回max,传回的就是地址。

对称二叉树🚩

给定一个二叉树,检查它是否是镜像对称的。

二叉树 [1,2,2,3,4,4,3] 是对称的。
但是 [1,2,2,null,3,null,3] 则不是镜像对称的:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

分析

镜像,镜子,对称···

几个关键词连在一起,一个想法顿时从脑海中浮现:

“怎么用递归啊?”

于是苦苦思索,最后灵光一现:

“去看看题解的思路!”

然后我回来了,然后开始写按照思路写代码。思路的传送门请点这儿

我应该是不能分析得比这个更加完美了。于是我来分享一下我看完思路的想法。

首先是关于解题方案中短路内容,因为初次接触递归,我也不知晓短路中的“后面不再计算”如何实现,于是自己摸索了一阵子。

另外是对于镜像的理解,一株树从根节点,分为左右两棵子数,分别递归。遍历的时候,如果左边部分的结点向其左子结点移动,那么右边部分的结点就向其右子结点移动;如果左边部分的结点向其右子结点移动,那么右边部分的结点就向其左子结点移动。总结起来就是一句话:左右反着来

我也没有看别人的代码不知道自己的思路和别人是否一致,不过实践出真知,最后也通过了测试用例,如果大家有相较于我的代码思想或者代码实现更好的方法,欢迎在评论区留言告诉我~

下面将给出我原本程序简化之后的版本。

代码实现以及执行结果

bool mirror(struct TreeNode* lefttree, struct TreeNode* righttree){
    if(!lefttree && !righttree) return true;
    if((!lefttree && righttree) || (lefttree && !righttree) || lefttree->val!=righttree->val) return false;
    return mirror(lefttree->left, righttree->right) && mirror(lefttree->right, righttree->left);
}

bool isSymmetric(struct TreeNode* root){
    if(!root || (root && !root->left && !root->right)) return true;  //special value for null node
    struct TreeNode* lefttree = root->left, * righttree = root->right;
    if(!lefttree || !righttree) return false;
    return mirror(lefttree, righttree);
}

在这里插入图片描述

路径总和🚩

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

说明: 叶子节点是指没有子节点的节点。

在这里插入图片描述

分析

这道题的思路比较清晰:如果遇到叶子节点而且当时的路径和累加器为目标值sum的时候,我们就最终返回true。

代码实现以及执行结果

为了实现一旦存在满足要求的情况,就将递归层层退回来的要求,我思索着设了一个变量flag作为标志。一旦“flag”为1,那么就退出当前一层递归。

这里的“flag”其实是一个指针变量,存储的是标志器的地址。实际的定义为:

int* flag = (int *)malloc(sizeof(int));
*flag = 0;

为什么这么做呢,因为我的想法是将*flag作为最后hasPathSum的返回值,为了避免函数调用结束变量被清空而导致返回一个初始化的0,于是出此下策。

代码以及执行结果就贴在下面:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
// int count=0;
void addsum(struct TreeNode* root, int sum, int count, int* flag){
    if(!root || *flag) return;
    count += root->val;
    if(!root->left && !root->right){
        if(count == sum) *flag=1;
        return;
    }
    if(root->left) addsum(root->left, sum, count, flag);
    if(root->right) addsum(root->right, sum, count, flag);
}


bool hasPathSum(struct TreeNode* root, int sum){
    if(!root) return false;
    int count=0;

    int* flag = (int *)malloc(sizeof(int));
    *flag = 0;

    addsum(root, sum, count, flag);
    return *flag;
}

在这里插入图片描述

回过头来看执行结果,好像这个程序写得还不错。但是还有别的思想,就介绍在后面:

其他思想

通过阅读程序,我们可以发现,我们使用了一个路径计数器count来记录从根节点到当前节点的路径上之和。然后判断的时候是根据count和目标值sum的是否成立来判断。

但是反过来想,如果我们不需要那个路径计数器count,能不能实现同样的效果呢?

自然是可以的。

我们只要没经过一个结点,就将sum值减去结点的val。如此一来,判断的条件就变为是否是sum是否为0,是不是更精简了呢?

都看到这了,不点个👍就走是不是有点说不过去?╰( ̄ω ̄o)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AuthurLEE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值