diff --git a/README.md b/README.md index 9ac4f7ec..05cdf9a8 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ * 二叉树 * [关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A) + * [二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA) + * [二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg) (持续更新中....) @@ -214,6 +216,7 @@ |[0107.二叉树的层次遍历II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0107.二叉树的层次遍历II.md) |树 |简单|**广度优先搜索/队列/BFS**| |[0110.平衡二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0110.平衡二叉树.md) |树 |简单|**递归**| |[0111.二叉树的最小深度](https://github.com/youngyangyang04/leetcode/blob/master/problems/0111.二叉树的最小深度.md) |树 |简单|**递归** **队列/BFS**| +|[0112.路径总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0112.路径总和.md) |树 |简单|**深度优先搜索/递归** **回溯** **栈**| |[0131.分割回文串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0131.分割回文串.md) |回溯 |中等|**回溯**| |[0142.环形链表II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0142.环形链表II.md) |链表 |中等|**快慢指针/双指针**| |[0144.二叉树的前序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0144.二叉树的前序遍历.md) |树 |中等|**递归** **迭代/栈**| diff --git a/pics/112.路径总和.png b/pics/112.路径总和.png new file mode 100644 index 00000000..9069173f Binary files /dev/null and b/pics/112.路径总和.png differ diff --git a/pics/501.二叉搜索树中的众数.png b/pics/501.二叉搜索树中的众数.png new file mode 100644 index 00000000..a7197b08 Binary files /dev/null and b/pics/501.二叉搜索树中的众数.png differ diff --git a/pics/501.二叉搜索树中的众数1.png b/pics/501.二叉搜索树中的众数1.png new file mode 100644 index 00000000..200bc2fa Binary files /dev/null and b/pics/501.二叉搜索树中的众数1.png differ diff --git a/problems/0112.路径总和.md b/problems/0112.路径总和.md index 9c8e6a87..cfbc5c9b 100644 --- a/problems/0112.路径总和.md +++ b/problems/0112.路径总和.md @@ -1,27 +1,49 @@ ## 题目地址 ## 思路 -// 遍历单条边,还是遍历整个树,取最优数值!! -// 对啊,用sum来减法啊,免得多定义一个变量 -## C++ +相信大家看到千篇一律的写法: +``` +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); + } +}; +``` +**这种写法简短是简短,但其实对于读者理解不是很友好,没有很好的体现出递归的顺序已经背后的回溯。** -贼粗糙的写法 +**相信很多同学都疑惑递归的过程中究竟什么时候需要返回值,什么时候不需要返回值?** -深度优先遍历 +**如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。** + +而本题就要要搜索一条路径,使其上所有节点值相加等于目标和,所以递归需要返回值! + +如图所示: + + + +图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用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 && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回 - if (cur->left) { // 左 + if (cur->left) { // 左 (空节点不遍历) // 遇到叶子节点返回true,则直接返回true if (traversal(cur->left, count - cur->left->val)) return true; } - if (cur->right) { // 右 + if (cur->right) { // 右 (空节点不遍历) // 遇到叶子节点返回true,则直接返回true if (traversal(cur->right, count - cur->right->val)) return true; } @@ -36,7 +58,8 @@ public: }; ``` -其实本题一定是有回溯的,没有回溯,如果后撤重新找另一条路径呢,但是貌似以上代码中,**大家貌似没有感受到回溯,那是因为回溯在代码里隐藏起来了。** + +那么其实本题一定是有回溯的,没有回溯,如何后撤重新找另一条路径呢,但是貌似以上代码中,**大家貌似没有感受到回溯,那是因为回溯在代码里隐藏起来了。** 隐藏在`traversal(cur->left, count - cur->left->val)`这里, 因为把`count - cur->left->val` 直接作为参数传进去,函数结束,count自然恢复到原先的数值了。 @@ -79,3 +102,45 @@ public: } }; ``` + +如果使用栈模拟递归的话,那么如果做回溯呢? + +此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。 + +C++就我们用pair结构来存放这个栈里的元素。 + +定义为:`pair` pair<节点指针,路径数值> + +这个为栈里的一个元素。 + +如下代码是使用栈模拟的前序遍历,如下:(详细注释) + +``` +class Solution { + +public: + bool hasPathSum(TreeNode* root, int sum) { + if (root == NULL) return false; + // 此时栈里要放的是pair<节点指针,路径数值> + stack> st; + st.push(pair(root, root->val)); + while (!st.empty()) { + pair node = st.top(); + st.pop(); + // 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true + if (!node.first->left && !node.first->right && sum == node.second) return true; + + // 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来 + if (node.first->right) { + st.push(pair(node.first->right, node.second + node.first->right->val)); + } + + // 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来 + if (node.first->left) { + st.push(pair(node.first->left, node.second + node.first->left->val)); + } + } + return false; + } +}; +``` diff --git a/problems/0501.二叉搜索树中的众数.md b/problems/0501.二叉搜索树中的众数.md new file mode 100644 index 00000000..2781d5c2 --- /dev/null +++ b/problems/0501.二叉搜索树中的众数.md @@ -0,0 +1,179 @@ +## 题目地址 + +https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/solution/ + +# 思路 + + +## 暴力统计 + +这看到这道题目,最直观的方法一定是把这个树都遍历了,用map统计频率,用vector排个序,最后出去前面高频的元素。 + +其实这是可以的,也是有效的,面试中时间紧张,可能快速的把这个方法实现出来,后面在考虑满满优化。 + + +至于用前中后序那种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病! + +这种方法C++代码如下: + + +``` +class Solution { +private: + +void searchBST(TreeNode* cur, unordered_map& map) { // 前序遍历 + if (cur == NULL) return ; + map[cur->val]++; // 统计元素频率 + searchBST(cur->left, map); + searchBST(cur->right, map); + return ; +} +bool static cmp (const pair& a, const pair& b) { + return a.second > b.second; +} +public: + vector findMode(TreeNode* root) { + unordered_map map; + vector result; + if (root == NULL) return result; + searchBST(root, map); + vector> vec(map.begin(), map.end()); + sort(vec.begin(), vec.end(), cmp); // 给频率排个序 + result.push_back(vec[0].first); + for (int i = 1; i < vec.size(); i++) { + if (vec[i].second == vec[0].second) result.push_back(vec[i].first); + else break; + } + return result; + } +}; +``` + +**这种方法的缺点是没有利用上二叉搜索树这一特性**,如果用这种方法,这道题就可以是普通的二叉树就行了,反正都要全撸一遍统计频率。 + +## 中序遍历 + +既然是搜索树,它就是有序的,那么如何有序呢? + +**搜索树在中序遍历的过程中,就是有序序列,所以此时的问题相当于 给出如果给出一个有序数组,求最大频率的元素集合。** + +**所以我们要采用中序遍历!** + +如图: + + + +中序遍历代码如下: + +``` + void searchBST(TreeNode* cur) { + if (cur == NULL) return ; + searchBST(cur->left); // 左 + (处理节点) // 中 + searchBST(cur->right); // 右 + return ; + } +``` + +遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,要是数组的话,好搞,在树上怎么搞呢? + +需要弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。 + +而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素,然后再给pre赋值即pre = cur; + +代码如下: + +``` + if (pre == NULL) { // 第一个节点 + count = 1; + } else if (pre->val == cur->val) { // 与前一个节点数值相同 + count++; + } else { // 与前一个节点数值不同 + count = 1; + } + pre = cur; // 更新上一个节点 +``` + +此时又有问题了,因为要求最大频率的元素集合,直观想的想法是要先遍历一遍找出频率最大的次数maxCount,然后在重新遍历一遍再把出现频率为maxCount的元素放进集合。 + + +那么如何只遍历一遍呢? + +如果 频率count 等于 maxCount,当然要把这个元素加入到结果集中(以下代码为result数组),代码如下: + +``` + if (count == maxCount) { // 如果和最大值相同,放进result中 + result.push_back(cur->val); + } +``` + +当时感觉这里有问题,result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大值呢。 + +所以下面要做如下操作: + +频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。 + +``` + if (count > maxCount) { // 如果计数大于最大值 + maxCount = count; + result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 + result.push_back(cur->val); + } +``` + +关键代码都讲完了,完整代码如下: + + +``` +class Solution { +private: + int count; + int maxCount; + TreeNode* pre; + vector result; + void searchBST(TreeNode* cur) { + if (cur == NULL) return ; + + searchBST(cur->left); // 左 + // 中 + if (pre == NULL) { // 第一个节点 + count = 1; + } else if (pre->val == cur->val) { // 与前一个节点数值相同 + count++; + } else { // 与前一个节点数值不同 + count = 1; + } + pre = cur; // 更新上一个节点 + + if (count == maxCount) { // 如果和最大值相同,放进result中 + result.push_back(cur->val); + } + + if (count > maxCount) { // 如果计数大于最大值 + maxCount = count; + result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 + result.push_back(cur->val); + } + + searchBST(cur->right); // 右 + return ; + } + +public: + vector findMode(TreeNode* root) { + int count = 0; // 记录元素出现次数 + int maxCount = 0; + TreeNode* pre = NULL; // 记录前一个节点 + result.clear(); + + searchBST(root); + return result; + } +}; +``` + +此时的运行效率: + + + +**需要强调的是 leetcode上的耗时统计是非常不准确的,看个大概就行,一样的代码耗时可以差百分之50以上**,所以leetcode的耗时统计别太当回事,知道理论上的效率优劣就行了。 diff --git a/problems/二叉树的递归遍历.md b/problems/二叉树的递归遍历.md new file mode 100644 index 00000000..1bc9a00b --- /dev/null +++ b/problems/二叉树的递归遍历.md @@ -0,0 +1,100 @@ + +# 二叉树: 一入递归深似海,从此offer是路人 + +> 一看就会,一写就废! + +这次我们要好好谈一谈递归,为什么很多同学看递归算法都是“一看就会,一写就废”。 + +主要是对递归不成体系,没有方法论,**每次写递归算法 ,都是靠玄学来写代码**,代码能不能编过都靠运气。 + +**本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。** + +这里帮助大家确定下来递归算法的三个要素。**每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!** + +1. **确定递归函数的参数和返回值:** +确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。 + +2. **确定终止条件:** +写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。 + +3. **确定单层递归的逻辑:** +确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。 + +好了,我们确认了递归的三要素,接下来就来练练手: + +**以下以前序遍历为例:** + +1. **确定递归函数的参数和返回值**:因为要打印出前序遍历节点的数值,所以参数里需要传入vector在放节点的数值,除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下: + +``` +void traversal(TreeNode* cur, vector& vec) +``` + +2. **确定终止条件**:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下: + +``` +if (cur == NULL) return; +``` + +3. **确定单层递归的逻辑**:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下: + +``` +vec.push_back(cur->val); // 中 +traversal(cur->left, vec); // 左 +traversal(cur->right, vec); // 右 +``` + +单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了,在看一下完整代码: + +前序遍历: + +``` +class Solution { +public: + void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + vec.push_back(cur->val); // 中 + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 + } + vector preorderTraversal(TreeNode* root) { + vector result; + traversal(root, result); + return result; + } +}; +``` + +那么前序遍历写出来之后,中序和后序遍历就不难理解了,代码如下: + +中序遍历: + +``` + void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + vec.push_back(cur->val); // 中 + traversal(cur->right, vec); // 右 + } +``` + +后序遍历: + +``` + void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 + vec.push_back(cur->val); // 中 + } +``` + +此时大家可以做一做leetcode上三道题目,分别是: + +* 144.二叉树的前序遍历 +* 145.二叉树的后序遍历 +* 94.二叉树的中序遍历 + +可能有同学感觉前后中序遍历的递归太简单了,要打迭代法(非递归),别急,我们明天打迭代法,打个通透! + +