This commit is contained in:
youngyangyang04
2020-10-08 10:33:40 +08:00
parent 61f88b332c
commit dd2d8a0718
8 changed files with 187 additions and 49 deletions

View File

@ -97,6 +97,7 @@
* [还在玩耍的你,该总结啦!(本周小结之二叉树)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg) * [还在玩耍的你,该总结啦!(本周小结之二叉树)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg)
* [二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) * [二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)
* [二叉树:做了这么多题目了,我的左叶子之和是多少?](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) * [二叉树:做了这么多题目了,我的左叶子之和是多少?](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA)
* [二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 104 KiB

BIN
pics/112.路径总和1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,29 +1,41 @@
## 题目地址 ## 题目地址
> 递归函数什么时候需要返回值
# 112. 路径总和 # 112. 路径总和
## 思路 给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
相信大家看到千篇一律的写法: 说明: 叶子节点是指没有子节点的节点。
```
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum) {
if (root == NULL) return false;
if (!root->left && !root->right && sum == root->val) {
return true;
}
return hasPathSum(root->left, sum - root->val) || hasPathSum(root->right, sum - root->val);
}
};
```
**这种写法简短是简短,但其实对于读者理解不是很友好,没有很好的体现出递归的顺序已经背后的回溯。**
**相信很多同学都疑惑递归的过程中究竟什么时候需要返回值,什么时候不需要返回值?** 示例: 
给定如下二叉树,以及目标和 sum = 22
<img src='../pics/112.路径总和1.png' width=600> </img></div>
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
# 思路
这道题我们要遍历从根节点到叶子节点的的路径看看总和是不是目标和。
## 递归
可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树
1. 确定递归函数的参数和返回类型
参数需要二叉树的根节点还需要一个计数器这个计数器用来计算二叉树的一条边之和是否正好是目标和计数器为int型。
**再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?**
在文章[二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)中,我给出了一个结论:
**如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。** **如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。**
而本题就要要搜索一条路径,使其上所有节点值相加等于目标和,所以递归要返回值 在[二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)中,因为要遍历树的所有路径,找出深度最深的叶子节点,所以递归函数不要返回值
而本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢?
如图所示: 如图所示:
@ -31,41 +43,55 @@ public:
图中可以看出遍历的路线并不要遍历整棵树所以递归函数需要返回值可以用bool类型表示。 图中可以看出遍历的路线并不要遍历整棵树所以递归函数需要返回值可以用bool类型表示。
那么使用深度优先遍历的方式本题前中后序都可以无所谓来遍历二叉树如下代码我尽量将每一步清晰的表现出来C++代码如下: 所以代码如下:
``` ```
class Solution { bool traversal(TreeNode* cur, int count) // 注意函数的返回类型
private:
bool traversal(TreeNode* cur, int count) {
if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点并且计数为0
if (!cur->left && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回
if (cur->left) { // 左 (空节点不遍历)
// 遇到叶子节点返回true则直接返回true
if (traversal(cur->left, count - cur->left->val)) return true;
}
if (cur->right) { // 右 (空节点不遍历)
// 遇到叶子节点返回true则直接返回true
if (traversal(cur->right, count - cur->right->val)) return true;
}
return false;
}
public:
bool hasPathSum(TreeNode* root, int sum) {
if (root == NULL) return false;
return traversal(root, sum - root->val);
}
};
``` ```
那么其实本题一定是有回溯的,没有回溯,如何后撤重新找另一条路径呢,但是貌似以上代码中,**大家貌似没有感受到回溯,那是因为回溯在代码里隐藏起来了。** 2. 确定终止条件
隐藏在`traversal(cur->left, count - cur->left->val)`这里, 因为把`count - cur->left->val` 直接作为参数传进去函数结束count自然恢复到原先的数值了。 首先计数器如何统计这一条路径的和呢?
为了把回溯的过程体现出来,将`if (traversal(cur->left, count - cur->left->val)) return true;` 改为如下代码: 不要去累加然后判断是否等于目标和那么代码比较麻烦可以用递减让计数器count初始为目标和然后每次减去遍历路径节点上的数值。
如果最后count == 0同时到了叶子节点的话说明找到了目标和。
如果遍历到了叶子节点count不为0就是没找到。
递归终止条件代码如下:
```
if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点并且计数为0
if (!cur->left && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回
```
3. 确定单层递归的逻辑
因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。
递归函数是有返回值的如果递归函数返回true说明找到了合适的路径应该立刻返回。
代码如下:
```
if (cur->left) { // 左 (空节点不遍历)
// 遇到叶子节点返回true则直接返回true
if (traversal(cur->left, count - cur->left->val)) return true; // 注意这里有回溯的逻辑
}
if (cur->right) { // 右 (空节点不遍历)
// 遇到叶子节点返回true则直接返回true
if (traversal(cur->right, count - cur->right->val)) return true; // 注意这里有回溯的逻辑
}
return false;
```
以上代码中是包含着回溯的,没有回溯,如何后撤重新找另一条路径呢。
回溯隐藏在`traversal(cur->left, count - cur->left->val)`这里, 因为把`count - cur->left->val` 直接作为参数传进去函数结束count的数值没有改变。
为了把回溯的过程体现出来,可以改为如下代码:
``` ```
if (cur->left) { // 左 if (cur->left) { // 左
@ -73,9 +99,16 @@ if (cur->left) { // 左
if (traversal(cur->left, count)) return true; if (traversal(cur->left, count)) return true;
count += cur->left->val; // 回溯,撤销处理结果 count += cur->left->val; // 回溯,撤销处理结果
} }
if (cur->right) { // 右
count -= cur->right->val;
if (traversal(cur->right, count - cur->right->val)) return true;
count += cur->right->val;
}
return false;
``` ```
这样大家就能感受到回溯了,整体回溯代码如下:
整体代码如下:
``` ```
class Solution { class Solution {
@ -105,9 +138,29 @@ public:
}; };
``` ```
以上代码精简之后如下:
```
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum) {
if (root == NULL) return false;
if (!root->left && !root->right && sum == root->val) {
return true;
}
return hasPathSum(root->left, sum - root->val) || hasPathSum(root->right, sum - root->val);
}
};
```
**是不是发现精简之后的代码,已经完全看不出分析的过程了,所以我们要把题目分析清楚之后,在追求代码精简。** 这一点我已经强调很多次了!
## 迭代
如果使用栈模拟递归的话,那么如果做回溯呢? 如果使用栈模拟递归的话,那么如果做回溯呢?
此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。 **此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。**
C++就我们用pair结构来存放这个栈里的元素。 C++就我们用pair结构来存放这个栈里的元素。
@ -146,3 +199,85 @@ public:
} }
}; };
``` ```
如果大家完全理解了本地的递归方法之后就可以顺便把leetcode上113. 路径总和II做了。
# 113. 路径总和II
给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树以及目标和 sum = 22
<img src='../pics/113.路径总和II1.png' width=600> </img></div>
## 思路
113.路径总和II要遍历整个树找到所有路径**所以递归函数不要返回值!**
如图:
<img src='../pics/113.路径总和II.png' width=600> </img></div>
为了尽可能的把细节体现出来,我写出如下代码(**这份代码并不简洁,但是逻辑非常清晰**
```
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
// 递归函数不需要返回值,因为我们要遍历整个树
void traversal(TreeNode* cur, int count) {
if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点切找到了和为sum的路径
result.push_back(path);
return;
}
if (!cur->left && !cur->right) return ; // 遇到叶子节点而没有找到合适的边,直接返回
if (cur->left) { // 左 (空节点不遍历)
path.push_back(cur->left->val);
count -= cur->left->val;
traversal(cur->left, count); // 递归
count += cur->left->val; // 回溯
path.pop_back(); // 回溯
}
if (cur->right) { // 右 (空节点不遍历)
path.push_back(cur->right->val);
count -= cur->right->val;
traversal(cur->right, count); // 递归
count += cur->right->val; // 回溯
path.pop_back(); // 回溯
}
return ;
}
public:
vector<vector<int>> pathSum(TreeNode* root, int sum) {
result.clear();
path.clear();
if (root == NULL) return result;
path.push_back(root->val); // 把根节点放进路径
traversal(root, sum - root->val);
return result;
}
};
```
至于113. 路径总和II 的迭代法我并没有写,用迭代方式记录所有路径比较麻烦,也没有必要,如果大家感兴趣的话,可以再深入研究研究。
# 总结
本篇通过leetcode上112. 路径总和 和 113. 路径总和II 详细的讲解了 递归函数什么时候需要返回值,什么不需要返回值。
这两道题目是掌握这一知识点非常好的题目,大家看完本篇文章再去做题,就会感受到搜索整棵树和搜索某一路径的差别。
对于112. 路径总和,我依然给出了递归法和迭代法,这种题目其实用迭代法会复杂一些,能掌握递归方式就够了!
今天是长假最后一天了,内容多一些,也是为了尽快让大家恢复学习状态,哈哈。
加个油!

View File

@ -18,7 +18,7 @@
如图: 如图:
<img src='../pics/107.二叉树的层次遍历II.png' width=600> </img></div> <img src='../pics/113.路径总和II.png' width=600> </img></div>
这道题目其实比[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)简单一些,大家做完了本题,可以在做[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)。 这道题目其实比[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)简单一些,大家做完了本题,可以在做[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)。

View File

@ -135,7 +135,7 @@ private:
} else { // 如果不是则直接跳过 } else { // 如果不是则直接跳过
continue; continue;
} }
backtracking(s, i + 1, path); // 寻找i+1为起始位置的子串 backtracking(s, i + 1); // 寻找i+1为起始位置的子串
path.pop_back(); // 回溯过程,弹出本次已经填在的子串 path.pop_back(); // 回溯过程,弹出本次已经填在的子串
} }
} }

View File

@ -207,6 +207,8 @@ public:
那么在如上代码中,**貌似没有看到回溯的逻辑,其实不然,回溯就隐藏在`traversal(cur->left, path + "->", result);`中的 `path + "->"`。** 每次函数调用完path依然是没有加上"->" 的,这就是回溯了。 那么在如上代码中,**貌似没有看到回溯的逻辑,其实不然,回溯就隐藏在`traversal(cur->left, path + "->", result);`中的 `path + "->"`。** 每次函数调用完path依然是没有加上"->" 的,这就是回溯了。
**如果这里还不理解的话,可以看这篇[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA),我这这篇中详细的解释了递归中如何隐藏着回溯。 **
**综合以上,第二种递归的代码虽然精简但把很多重要的点隐藏在了代码细节里,第一种递归写法虽然代码多一些,但是把每一个逻辑处理都完整的展现了出来了。** **综合以上,第二种递归的代码虽然精简但把很多重要的点隐藏在了代码细节里,第一种递归写法虽然代码多一些,但是把每一个逻辑处理都完整的展现了出来了。**