diff --git a/README.md b/README.md index d1430d96..2f204e10 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ * [回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) * [本周小结!(回溯算法系列一)](https://mp.weixin.qq.com/s/m2GnTJdkYhAamustbb6lmw) * [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw) + * [回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) (持续更新中....) @@ -405,6 +406,7 @@ |[0841.钥匙和房间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0841.钥匙和房间.md) |孤岛问题 |中等|**bfs** **dfs**| |[0844.比较含退格的字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0844.比较含退格的字符串.md) |字符串 |简单|**栈** **双指针优化** 使用栈的思路但没有必要使用栈| |[0925.长按键入](https://github.com/youngyangyang04/leetcode/blob/master/problems/0925.长按键入.md) |字符串 |简单|**双指针/模拟** 是一道模拟类型的题目| +|[0941.有效的山脉数组](https://github.com/youngyangyang04/leetcode/blob/master/problems/0941.有效的山脉数组.md) |数组 |简单|**双指针**| |[0968.监控二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0968.监控二叉树.md) |二叉树 |困难|**贪心** 贪心与二叉树的结合| |[0977.有序数组的平方](https://github.com/youngyangyang04/leetcode/blob/master/problems/0977.有序数组的平方.md) |数组 |中等|**双指针** 还是比较巧妙的| |[1002.查找常用字符](https://github.com/youngyangyang04/leetcode/blob/master/problems/1002.查找常用字符.md) |栈 |简单|**栈**| diff --git a/pics/131.分割回文串.png b/pics/131.分割回文串.png index e9c67546..3c282d6d 100644 Binary files a/pics/131.分割回文串.png and b/pics/131.分割回文串.png differ diff --git a/pics/57.插入区间.png b/pics/57.插入区间.png new file mode 100644 index 00000000..67290167 Binary files /dev/null and b/pics/57.插入区间.png differ diff --git a/pics/57.插入区间1.png b/pics/57.插入区间1.png new file mode 100644 index 00000000..69835dee Binary files /dev/null and b/pics/57.插入区间1.png differ diff --git a/pics/941.有效的山脉数组.png b/pics/941.有效的山脉数组.png new file mode 100644 index 00000000..e261242c Binary files /dev/null and b/pics/941.有效的山脉数组.png differ diff --git a/problems/0053.最大子序和.md b/problems/0053.最大子序和.md index da30c52d..339f97c0 100644 --- a/problems/0053.最大子序和.md +++ b/problems/0053.最大子序和.md @@ -2,6 +2,8 @@ ## 题目地址 https://leetcode-cn.com/problems/maximum-subarray/ +# 思路 + ## 暴力解法 暴力解法的思路,第一层for 就是设置起始位置,第二层for循环遍历数组寻找最大值 @@ -27,12 +29,30 @@ public: ``` ## 贪心解法 -贪心解法,其实不是很好理解, 看上面暴力的解法是两层for循环,那如何省掉一层for循环呢 - -其实**暴力解法中设置起始位置这个for循环是可以省略掉的**,这也是关键的一步,有了这个思路之后,就可以看一下代码了 -时间复杂度:O(n) -空间复杂度:O(1) +贪心解法,其实不是很好理解, 看上面暴力的解法是两层for循环,那如何省掉一层for循环呢 + +**贪心贪的是哪里呢?** + +如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方! + +同样的道理,遍历nums,从头开始用count累积,如果count一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从头累积count了(也就是此时count要归0),因为已经变为负数的count,只会拖累总和。 + +相当于是不断调整区间的起始位置。 + + +**那有同学问了,区间终止位置不用调整么? 如何才能得到最大子序和呢?** + +区间的终止位置,其实就是如果count取到最大值了,用result记录一下就可以了。 + +如动画所示: + + + +红色的其实位置就是贪心每次取count为正数的时候,开始一个区间的统计。 + +不难写出如下C++代码(关键地方已经注释) + ``` class Solution { public: @@ -41,14 +61,18 @@ public: int count = 0; for (int i = 0; i < nums.size(); i++) { count += nums[i]; - if (count > result) { // 相当于每次取各个终点的最大值 + if (count > result) { // 取区间累计的最大值(相当于不断确定最大子序终止位置) result = count; } - if (count <= 0) count = 0; // 相当于重置起点,因为遇到负数一定是拉低总体数值的 + if (count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和 } return result; } }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +时间复杂度:O(n) +空间复杂度:O(1) + +> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),期待你的关注! + diff --git a/problems/0057.插入区间.md b/problems/0057.插入区间.md new file mode 100644 index 00000000..8d6ecc42 --- /dev/null +++ b/problems/0057.插入区间.md @@ -0,0 +1,86 @@ + +这道题目合并的情况有很多种,想想都让人头疼。 + +我把这道题目化为三步: + +## 步骤一:找到需要合并的区间 + +找到插入区间需要插入或者合并的位置。 + +代码如下: + +``` +int index = 0; // intervals的索引 +while (index < intervals.size() && intervals[index][1] < newInterval[0]) { + result.push_back(intervals[index++]); +} +``` + +此时intervals[index]就需要合并的区间了 + +## 步骤二:合并区间 + +合并区间还有两种情况 + +1. intervals[index]需要合并,如图: + + + +对于这种情况,只要是intervals[index]起始位置 <= newInterval终止位置,就要一直合并下去。 + +代码如下: + +``` +while (index < intervals.size() && intervals[index][0] <= newInterval[1]) { // 注意防止越界 + newInterval[0] = min(intervals[index][0], newInterval[0]); + newInterval[1] = max(intervals[index][1], newInterval[1]); + index++; +} +``` +合并之后,将newInterval放入result就可以了 + +2. intervals[index]不用合并,插入区间直接插入就行,如图: + + + +对于这种情况,就直接把newInterval放入result就可以了 + +## 步骤三:处理合并区间之后的区间 + +合并之后,就应该把合并之后的区间,以此加入result中。 + +代码如下: + +``` +while (index < intervals.size()) { + result.push_back(intervals[index++]); +} +``` + +# 整体C++代码 + +``` +class Solution { +public: + vector> insert(vector>& intervals, vector& newInterval) { + vector> result; + int index = 0; // intervals的索引 + // 步骤一:找到需要合并的区间 + while (index < intervals.size() && intervals[index][1] < newInterval[0]) { + result.push_back(intervals[index++]); + } + // 步骤二:合并区间 + while (index < intervals.size() && intervals[index][0] <= newInterval[1]) { + newInterval[0] = min(intervals[index][0], newInterval[0]); + newInterval[1] = max(intervals[index][1], newInterval[1]); + index++; + } + result.push_back(newInterval); + // 步骤三:处理合并区间之后的区间 + while (index < intervals.size()) { + result.push_back(intervals[index++]); + } + return result; + } +}; +``` diff --git a/problems/0131.分割回文串.md b/problems/0131.分割回文串.md index f46a6149..2d72bf1e 100644 --- a/problems/0131.分割回文串.md +++ b/problems/0131.分割回文串.md @@ -1,10 +1,24 @@ -## 题目地址 -https://leetcode-cn.com/problems/palindrome-partitioning/ +> 切割问题其实是一种组合问题! -## 思路 +# 131.分割回文串 -(分割 有这么多系列啊!!!!!) +题目链接:https://leetcode-cn.com/problems/palindrome-partitioning/ + +给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。 + +返回 s 所有可能的分割方案。 + +示例: +输入: "aab" +输出: +[ + ["aa","b"], + ["a","a","b"] +] + + +# 思路 本题这涉及到两个关键问题: @@ -13,17 +27,16 @@ https://leetcode-cn.com/problems/palindrome-partitioning/ 相信这里不同的切割方式可以搞懵很多同学了。 +这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯。 -这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯 +一些同学可能想不清楚 回溯究竟是如果切割字符串呢? -一些同学可能想不清楚 回溯究竟是如果切割子串 +我们来分析一下切割,**其实切割问题类似组合问题**。 -我们来分析一下切割,其实切割,类似于组合问题。 +例如对于字符串abcdef: -例如对于字符串abcdef, - -组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中在选组第三个.....。 -切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段.....。 +* 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中在选组第三个.....。 +* 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段.....。 感受出来了不? @@ -35,29 +48,36 @@ https://leetcode-cn.com/problems/palindrome-partitioning/ 此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。 -回溯法模板: +## 回溯三部曲 -``` -backtracking() { - if (终止条件) { - 存放结果; - } +* 递归函数参数 - for (选择:选择列表(可以想成树中节点孩子的数量)) { - 递归,处理节点; - backtracking(); - 回溯,撤销处理结果 - } -} +全局变量数组path存放切割后回文的子串,二维数组result存放结果集。 (这两个参数可以放到函数参数里) -``` +本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。 -在来分析一下终止条件,要从上图中可以看出,切割线切到了字符串最后面,说明找到了一种切割方法,那么此时,就是本层递归的终止终止条件。 - -那么在代码里什么是切割线呢,在处理集合问题的时候,参数我们都要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线。 +在[回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)中我们深入探讨了组合问题什么时候需要startIndex,什么时候不需要startIndex。 代码如下: +``` +vector> result; +vector path; // 放已经回文的子串 +void backtracking (const string& s, int startIndex) { +``` + +* 递归函数终止条件 + + + +从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止终止条件。 + +**那么在代码里什么是切割线呢?** + +在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线。 + +所以终止条件代码如下: + ``` void backtracking (const string& s, int startIndex) { // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 @@ -68,7 +88,9 @@ void backtracking (const string& s, int startIndex) { } ``` -在来看看在递归循环,中如何截取子串呢? +* 单层搜索的逻辑 + +**来看看在递归循环,中如何截取子串呢?** 在`for (int i = startIndex; i < s.size(); i++)`循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。 @@ -82,27 +104,25 @@ for (int i = startIndex; i < s.size(); i++) { // 获取[startIndex,i]在s中的子串 string str = s.substr(startIndex, i - startIndex + 1); path.push_back(str); - } else { // 如果不是则直接跳过 + } else { // 如果不是则直接跳过 continue; } + backtracking(s, i + 1); // 寻找i+1为起始位置的子串 + path.pop_back(); // 回溯过程,弹出本次已经填在的子串 } ``` -然后就开始递归与回溯的过程,递归的时候,我们要传入i+1 作为下一轮递归的遍历的其实位置(同样也就是切割线)。 大家可以发现在处理组合问题中,也要传入的是i+1,所以 切割问题和组合问题是最像的。 +**注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1**。 -代码如下: - -``` -backtracking(s, i + 1, path); // 寻找i+1为起始位置的子串 -path.pop_back(); // 回溯过程,弹出本次已经填在的子串 -``` +## 判断回文子串 最后我们看一下回文子串要如何判断了,判断一个字符串是否是回文。 可以使用双指针法,一个指针从前向后,一个指针从后先前,如果前后指针所指向的元素是相等的,就是回文字符串了。 -代码如下: -``` +那么判断回文的C++代码如下: + +```C++ bool isPalindrome(const string& s, int start, int end) { for (int i = start, j = end; i < j; i++, j--) { if (s[i] != s[j]) { @@ -112,15 +132,38 @@ path.pop_back(); // 回溯过程,弹出本次已经填在的子串 return true; } ``` + +如果大家对双指针法有生疏了,传送门:[双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) + 此时关键代码已经讲解完毕,整体代码如下(详细注释了) -## C++代码 +# C++整体代码 + +根据Carl给出的回溯算法模板: + ``` +void backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} + +``` + +不难写出如下代码: + +```C++ class Solution { private: vector> result; vector path; // 放已经回文的子串 - // startIndex: 搜索的起始位置 void backtracking (const string& s, int startIndex) { // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 if (startIndex >= s.size()) { @@ -128,11 +171,11 @@ private: return; } for (int i = startIndex; i < s.size(); i++) { - if (isPalindrome(s, startIndex, i)) { // 是回文子串 + if (isPalindrome(s, startIndex, i)) { // 是回文子串 // 获取[startIndex,i]在s中的子串 string str = s.substr(startIndex, i - startIndex + 1); path.push_back(str); - } else { // 如果不是则直接跳过 + } else { // 不是回文,跳过 continue; } backtracking(s, i + 1); // 寻找i+1为起始位置的子串 @@ -156,4 +199,40 @@ public: } }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +# 总结 + +这道题目在leetcode上是中等,但可以说是hard的题目了,但是代码其实就是按照模板的样子来的。 + +那么难究竟难在什么地方呢? + +**我列出如下几个难点:** + +* 切割问题可以抽象为组合问题 +* 如何模拟那些切割线 +* 切割问题中递归如何终止 +* 在递归循环中如何截取子串 +* 如何判断回文 + +**我们平时在做难题的时候,总结出来难究竟难在哪里也是一种需要锻炼的能力**。 + +一些同学可能遇到题目比较难,但是不知道题目难在哪里,反正就是很难。其实这样还是思维不够清晰,这种总结的能力需要多接触多锻炼。 + +**本题我相信很多同学主要卡在了第一个难点上:就是不知道如何切割,甚至知道要用回溯法,也不知道如何用。也就是没有体会到按照求组合问题的套路就可以解决切割**。 + +如果意识到这一点,算是重大突破了。接下来就可以对着模板照葫芦画瓢。 + +**但接下来如何模拟切割线,如何终止,如何截取子串,其实都不好想,最后判断回文算是最简单的了**。 + +除了这些难点,**本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1**。 + +所以本题应该是一个道hard题目了。 + +**可能刷过这道题目的录友都没感受到自己原来克服了这么多难点,就把这道题目AC了**,这应该叫做无招胜有招,人码合一,哈哈哈。 + +当然,本题131.分割回文串还可以用暴力搜索一波,132.分割回文串II和1278.分割回文串III 爆搜就会超时,需要使用动态规划了,我们会在动态规划系列中,详细讲解! + +**就酱,如果感觉「代码随想录」不错,就把Carl宣传一波吧!** + +> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),期待你的关注! + diff --git a/problems/0941.有效的山脉数组.md b/problems/0941.有效的山脉数组.md new file mode 100644 index 00000000..85a9c745 --- /dev/null +++ b/problems/0941.有效的山脉数组.md @@ -0,0 +1,44 @@ +## 题目链接 + +https://leetcode-cn.com/problems/valid-mountain-array/ + +## 思路 + +判断是山峰,主要就是要严格的保存左边到中间,和右边到中间是递增的。 + +这样可以使用两个指针,left和right,让其按照如下规则移动,如图: + + + +**注意这里还是有一些细节,例如如下两点:** + +* 因为left和right是数组下表,移动的过程中注意不要数组越界 +* 如果left或者right没有移动,说明是一个单调递增或者递减的数组,依然不是山峰 + +C++代码如下: + +``` +class Solution { +public: + bool validMountainArray(vector& A) { + if (A.size() < 3) return false; + int left = 0; + int right = A.size() - 1; + + // 注意防止越界 + while (left < A.size() - 1 && A[left] < A[left + 1]) left++; + + // 注意防止越界 + while (right > 0 && A[right] < A[right - 1]) right--; + + // 如果left或者right都在起始位置,说明不是山峰 + if (left == right && left != 0 && right != A.size() - 1) return true; + return false; + } +}; +``` +如果想系统学一学双指针的话, 可以看一下这篇[双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) + + +> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),期待你的关注! + diff --git a/video/53.最大子序和.gif b/video/53.最大子序和.gif new file mode 100644 index 00000000..7514a5ac Binary files /dev/null and b/video/53.最大子序和.gif differ diff --git a/video/53.最大子序和.mp4 b/video/53.最大子序和.mp4 new file mode 100644 index 00000000..94b62a73 Binary files /dev/null and b/video/53.最大子序和.mp4 differ