diff --git a/README.md b/README.md index 5d91b0d2..bc3927d1 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ * [还在玩耍的你,该总结啦!(本周小结之二叉树)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg) * [二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) * [二叉树:做了这么多题目了,我的左叶子之和是多少?](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) + * [二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw) diff --git a/pics/112.路径总和.png b/pics/112.路径总和.png index 9069173f..2a1b5100 100644 Binary files a/pics/112.路径总和.png and b/pics/112.路径总和.png differ diff --git a/pics/112.路径总和1.png b/pics/112.路径总和1.png new file mode 100644 index 00000000..4c6c0f60 Binary files /dev/null and b/pics/112.路径总和1.png differ diff --git a/pics/113.路径总和II1.png b/pics/113.路径总和II1.png new file mode 100644 index 00000000..e1d5a2d1 Binary files /dev/null and b/pics/113.路径总和II1.png differ diff --git a/problems/0112.路径总和.md b/problems/0112.路径总和.md index b616938f..70defb45 100644 --- a/problems/0112.路径总和.md +++ b/problems/0112.路径总和.md @@ -1,29 +1,41 @@ ## 题目地址 +> 递归函数什么时候需要返回值 + # 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, + + + +返回 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类型表示。 -那么使用深度优先遍历的方式(本题前中后序都可以,无所谓)来遍历二叉树,如下代码我尽量将每一步清晰的表现出来,C++代码如下: +所以代码如下: ``` -class Solution { -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); - } -}; +bool traversal(TreeNode* cur, int count) // 注意函数的返回类型 ``` -那么其实本题一定是有回溯的,没有回溯,如何后撤重新找另一条路径呢,但是貌似以上代码中,**大家貌似没有感受到回溯,那是因为回溯在代码里隐藏起来了。** +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) { // 左 @@ -73,9 +99,16 @@ if (cur->left) { // 左 if (traversal(cur->left, count)) return true; 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 { @@ -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结构来存放这个栈里的元素。 @@ -146,3 +199,85 @@ public: } }; ``` + +如果大家完全理解了本地的递归方法之后,就可以顺便把leetcode上113. 路径总和II做了。 + +# 113. 路径总和II + +给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。 + +说明: 叶子节点是指没有子节点的节点。 + +示例: +给定如下二叉树,以及目标和 sum = 22, + + + + +## 思路 + +113.路径总和II要遍历整个树,找到所有路径,**所以递归函数不要返回值!** + +如图: + + + + +为了尽可能的把细节体现出来,我写出如下代码(**这份代码并不简洁,但是逻辑非常清晰**) + +``` +class Solution { +private: + vector> result; + vector 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> 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. 路径总和,我依然给出了递归法和迭代法,这种题目其实用迭代法会复杂一些,能掌握递归方式就够了! + +今天是长假最后一天了,内容多一些,也是为了尽快让大家恢复学习状态,哈哈。 + +加个油! diff --git a/problems/0113.路径总和II.md b/problems/0113.路径总和II.md index 430e5d70..b12f27f2 100644 --- a/problems/0113.路径总和II.md +++ b/problems/0113.路径总和II.md @@ -18,7 +18,7 @@ 如图: - + 这道题目其实比[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)简单一些,大家做完了本题,可以在做[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)。 diff --git a/problems/0131.分割回文串.md b/problems/0131.分割回文串.md index 1e1244fb..f46a6149 100644 --- a/problems/0131.分割回文串.md +++ b/problems/0131.分割回文串.md @@ -135,7 +135,7 @@ private: } else { // 如果不是则直接跳过 continue; } - backtracking(s, i + 1, path); // 寻找i+1为起始位置的子串 + backtracking(s, i + 1); // 寻找i+1为起始位置的子串 path.pop_back(); // 回溯过程,弹出本次已经填在的子串 } } diff --git a/problems/0257.二叉树的所有路径.md b/problems/0257.二叉树的所有路径.md index 1e083c78..d63fa41d 100644 --- a/problems/0257.二叉树的所有路径.md +++ b/problems/0257.二叉树的所有路径.md @@ -207,6 +207,8 @@ public: 那么在如上代码中,**貌似没有看到回溯的逻辑,其实不然,回溯就隐藏在`traversal(cur->left, path + "->", result);`中的 `path + "->"`。** 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。 +**如果这里还不理解的话,可以看这篇[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA),我这这篇中详细的解释了递归中如何隐藏着回溯。 ** + **综合以上,第二种递归的代码虽然精简但把很多重要的点隐藏在了代码细节里,第一种递归写法虽然代码多一些,但是把每一个逻辑处理都完整的展现了出来了。**