diff --git a/README.md b/README.md index bc3927d1..89bceef7 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,16 @@ * [字符串:前缀表不右移,难道就写不出KMP了?](https://mp.weixin.qq.com/s/p3hXynQM2RRROK5c6X7xfw) * [字符串:总结篇!](https://mp.weixin.qq.com/s/gtycjyDtblmytvBRFlCZJg) -* [双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) +* 双指针法 + * [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) + * [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) + * [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) + * [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) + * [链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) + * [链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) + * [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) + * [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) + * [双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) * 栈与队列 * [栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ) diff --git a/pics/105. 从前序与中序遍历序列构造二叉树.png b/pics/105. 从前序与中序遍历序列构造二叉树.png new file mode 100644 index 00000000..7430be27 Binary files /dev/null and b/pics/105. 从前序与中序遍历序列构造二叉树.png differ diff --git a/pics/106. 从中序与后序遍历序列构造二叉树1.png b/pics/106. 从中序与后序遍历序列构造二叉树1.png new file mode 100644 index 00000000..ab8fa4b9 Binary files /dev/null and b/pics/106. 从中序与后序遍历序列构造二叉树1.png differ diff --git a/pics/106.从中序与后序遍历序列构造二叉树2.png b/pics/106.从中序与后序遍历序列构造二叉树2.png new file mode 100644 index 00000000..8726f4ce Binary files /dev/null and b/pics/106.从中序与后序遍历序列构造二叉树2.png differ diff --git a/problems/0106.从中序与后序遍历序列构造二叉树.md b/problems/0106.从中序与后序遍历序列构造二叉树.md index 6af49fd6..0089d3fc 100644 --- a/problems/0106.从中序与后序遍历序列构造二叉树.md +++ b/problems/0106.从中序与后序遍历序列构造二叉树.md @@ -1,20 +1,36 @@ ## 题目地址 https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/ +> 给出两个序列 + +# 106.从中序与后序遍历序列构造二叉树 + +根据一棵树的中序遍历与后序遍历构造二叉树。 + +注意: +你可以假设树中没有重复的元素。 + +例如,给出 + +中序遍历 inorder = [9,3,15,20,7] +后序遍历 postorder = [9,15,7,20,3] +返回如下的二叉树: + + + ## 思路 -首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以后序数组最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。 +首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。 如果让我们肉眼看两个序列,画一颗二叉树的话,应该分分钟都可以画出来。 流程如图: - 那么代码应该怎么写呢? -说道一层一层切割,就应该想到了递归。 +说到一层一层切割,就应该想到了递归。 来看一下一共分几步: @@ -22,7 +38,7 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde * 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。 -* 第三步:找到后序数组最后一个元素在后序数组的位置,作为切割点 +* 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点 * 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组) @@ -35,24 +51,26 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde ``` TreeNode* traversal (vector& inorder, vector& postorder) { + // 第一步 if (postorder.size() == 0) return NULL; - // 后序遍历数组最后一个元素,就是当前的中间节点 + // 第二步:后序遍历数组最后一个元素,就是当前的中间节点 int rootValue = postorder[postorder.size() - 1]; TreeNode* root = new TreeNode(rootValue); // 叶子节点 if (postorder.size() == 1) return root; - 找切割点 + // 第三步:找切割点 int delimiterIndex; for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { if (inorder[delimiterIndex] == rootValue) break; } - 切割中序数组,得到 中序左数组和中序右数组 - 切割后序数组,得到 后序左数组和后序右数组 + // 第四步:切割中序数组,得到 中序左数组和中序右数组 + // 第五步:切割后序数组,得到 后序左数组和后序右数组 + // 第六步 root->left = traversal(中序左数组, 后序左数组); root->right = traversal(中序右数组, 后序右数组); @@ -60,11 +78,11 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde } ``` -难点大家应该发现了,如何切割呢,边界值找不好很容易乱套。 +**难点大家应该发现了,就是如何切割,以及边界值找不好很容易乱套。** 此时应该注意确定切割的标准,是左闭右开,还有左开又闭,还是左闭又闭,这个就是不变量,要在递归中保持这个不变量。 -在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭又闭,必要乱套! +**在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭又闭,必然乱套!** 我在[数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)和[数组:这个循环可以转懵很多人!](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg)中都强调过循环不变量的重要性,在二分查找以及螺旋矩阵的求解中,坚持循环不变量非常重要,本题也是。 @@ -77,17 +95,16 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde ``` - // 找到中序遍历的切割点 - int delimiterIndex; - for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { - if (inorder[delimiterIndex] == rootValue) break; - } +// 找到中序遍历的切割点 +int delimiterIndex; +for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; +} - // 左闭右开区间 - // [0, delimiterIndex) - vector leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); - // [delimiterIndex + 1, end) - vector rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() ); +// 左闭右开区间:[0, delimiterIndex) +vector leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); +// [delimiterIndex + 1, end) +vector rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() ); ``` 接下来就要切割后序数组了。 @@ -96,7 +113,7 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde 后序数组的切割点怎么找? -后序数组没有明确的切割元素来进行左右切割,不想中序数组有明确的切割左右,左右分开就可以了。 +后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。 **此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。** @@ -105,28 +122,27 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde 代码如下: ``` - // postorder 舍弃末尾元素 - postorder.resize(postorder.size() - 1); +// postorder 舍弃末尾元素,因为这个元素就是中间节点,已经用过了 +postorder.resize(postorder.size() - 1); - // 依然左闭右开,注意这里使用了左中序数组大小作为切割点 - // [0, leftInorder.size) - vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size()); - // [leftInorder.size(), end) - vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end()); +// 左闭右开,注意这里使用了左中序数组大小作为切割点:[0, leftInorder.size) +vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size()); +// [leftInorder.size(), end) +vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end()); ``` -此时,中序数组切成了左中序数组和右中序数组,后序数组切割成序数组和右后序数组。 +此时,中序数组切成了左中序数组和右中序数组,后序数组切割成左后序数组和右后序数组。 接下来可以递归了,代码如下: ``` - root->left = traversal(leftInorder, leftPostorder); - root->right = traversal(rightInorder, rightPostorder); +root->left = traversal(leftInorder, leftPostorder); +root->right = traversal(rightInorder, rightPostorder); ``` 完整代码如下: -## C++ 完整代码 +### C++完整代码 ``` class Solution { @@ -206,6 +222,7 @@ private: vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size()); vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end()); + // 一下为日志 cout << "----------" << endl; cout << "leftInorder :"; @@ -244,11 +261,11 @@ public: }; ``` -**此时应该发现了,如上的代码性能并不好,应为每层递归定定义了新的vector,既耗时又耗空间,但是上面的代码是最好理解的,为了方便读者理解,所以用如上的代码来讲解。** +**此时应该发现了,如上的代码性能并不好,应为每层递归定定义了新的vector(就是数组),既耗时又耗空间,但上面的代码是最好理解的,为了方便读者理解,所以用如上的代码来讲解。** 下面给出用下表索引写出的代码版本:(思路是一样的,只不过不用重复定义vector了,每次用下表索引来分割) -## C++最终优化版本 +### C++优化版本 ``` class Solution { private: @@ -368,7 +385,24 @@ public: # 105. 从前序与中序遍历序列构造二叉树 -同样的道理 +根据一棵树的前序遍历与中序遍历构造二叉树。 + +注意: +你可以假设树中没有重复的元素。 + +例如,给出 + +前序遍历 preorder = [3,9,20,15,7] +中序遍历 inorder = [9,3,15,20,7] +返回如下的二叉树: + + + +## 思路 + +本题和106是一样的道理。 + +我就直接给出代码了。 带日志的版本C++代码如下: (**带日志的版本仅用于调试,不要在leetcode上提交,会超时**) @@ -444,9 +478,8 @@ public: }; ``` -105. 从前序与中序遍历序列构造二叉树,最后版本: +105.从前序与中序遍历序列构造二叉树,最后版本,C++代码: -C++代码: ``` class Solution { private: @@ -493,3 +526,45 @@ public: } }; ``` + +# 思考题 + +前序和中序可以唯一确定一颗二叉树。 + +后序和中序可以唯一确定一颗二叉树。 + +那么前序和后序可不可以唯一确定一颗二叉树呢? + +**前序和后序不能唯一确定一颗二叉树!**,因为没有中序遍历无法确定左右部分,也就是无法分割。 + +举一个例子: + + + +tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 + +tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 + +那么tree1 和 tree2 的前序和后序完全相同,这是一棵树么,很明显是两棵树! + +所以前序和后序不能唯一确定一颗二叉树! + +# 总结 + +之前我们讲的二叉树题目都是各种遍历二叉树,这次开始构造二叉树了,思路其实比较简单,但是真正代码实现出来并不容易。 + +所以要避免眼高手低,踏实的把代码写出来。 + +我同时给出了添加日志的代码版本,因为这种题目是不太容易写出来调一调就能过的,所以一定要把流程日志打出来,看看符不符合自己的思路。 + +大家遇到这种题目的时候,也要学会打日志来调试(如何打日志有时候也是个技术活),不要脑动模拟,脑动模拟很容易越想越乱。 + +最后我还给出了为什么前序和中序可以唯一确定一颗二叉树,后序和中序可以唯一确定一颗二叉树,而前序和后序却不行。 + +认真研究完本篇,相信大家对二叉树的构造会清晰很多。 + +如果学到了,就赶紧转发给身边需要的同学吧! + +加个油! + + diff --git a/problems/0111.二叉树的最小深度.md b/problems/0111.二叉树的最小深度.md index a4edce23..eebfd76e 100644 --- a/problems/0111.二叉树的最小深度.md +++ b/problems/0111.二叉树的最小深度.md @@ -165,20 +165,17 @@ public: queue que; que.push(root); while(!que.empty()) { - int size = que.size(); + int size = que.size(); depth++; // 记录最小深度 - int flag = 0; for (int i = 0; i < size; i++) { TreeNode* node = que.front(); que.pop(); if (node->left) que.push(node->left); if (node->right) que.push(node->right); if (!node->left && !node->right) { // 当左右孩子都为空的时候,说明是最低点的一层了,退出 - flag = 1; - break; + return depth; } } - if (flag == 1) break; } return depth; } diff --git a/problems/0141.环形链表.md b/problems/0141.环形链表.md new file mode 100644 index 00000000..4198ba4a --- /dev/null +++ b/problems/0141.环形链表.md @@ -0,0 +1,45 @@ + +## 思路 + +可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。 + +为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢? + +首先第一点: **fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。** + +那么来看一下,**为什么fast指针和slow指针一定会相遇呢?** + +可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。 + +会发现最终都是这种情况, 如下图: + + + +fast和slow各自再走一步, fast和slow就相遇了 + +这是因为fast是走两步,slow是走一步,**其实相对于slow来说,fast是一个节点一个节点的靠近slow的**,所以fast一定可以和slow重合。 + + +## C++代码如下 + +``` +class Solution { +public: + bool hasCycle(ListNode *head) { + ListNode* fast = head; + ListNode* slow = head; + while(fast != NULL && fast->next != NULL) { + slow = slow->next; + fast = fast->next->next; + // 快慢指针相遇,说明有环 + if (slow == fast) return true; + } + return false; + } +}; +``` +## 扩展 + +做完这道题目,可以在做做142.环形链表II,不仅仅要找环,还要找环的入口。 + +142.环形链表II题解:[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)