diff --git a/README.md b/README.md index 69c285bc..1577975d 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ * [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw) * [回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) * [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) + * [回溯算法:复原IP地址](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA) (持续更新中....) @@ -415,6 +416,7 @@ |[1002.查找常用字符](https://github.com/youngyangyang04/leetcode/blob/master/problems/1002.查找常用字符.md) |栈 |简单|**栈**| |[1047.删除字符串中的所有相邻重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/1047.删除字符串中的所有相邻重复项.md) |哈希表 |简单|**哈希表/数组**| |[1207.独一无二的出现次数](https://github.com/youngyangyang04/leetcode/blob/master/problems/1207.独一无二的出现次数.md) |哈希表 |简单|**哈希** 两层哈希| +|[1356.根据数字二进制下1的数目排序](https://github.com/youngyangyang04/leetcode/blob/master/problems/1356.根据数字二进制下1的数目排序.md) |位运算 |简单|**位运算** 巧妙的计算二进制中1的数量| |[1365.有多少小于当前数字的数字](https://github.com/youngyangyang04/leetcode/blob/master/problems/1365.有多少小于当前数字的数字.md) |数组、哈希表 |简单|**哈希** 从后遍历的技巧很不错| |[1382.将二叉搜索树变平衡](https://github.com/youngyangyang04/leetcode/blob/master/problems/1047.删除字符串中的所有相邻重复项.md) |二叉搜索树 |中等|**递归** **迭代** 98和108的组合题目| |[剑指Offer05.替换空格](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer05.替换空格.md) |字符串 |简单|**双指针**| diff --git a/pics/1356.根据数字二进制下1的数目排序.png b/pics/1356.根据数字二进制下1的数目排序.png new file mode 100644 index 00000000..0fca9fed Binary files /dev/null and b/pics/1356.根据数字二进制下1的数目排序.png differ diff --git a/pics/376.摆动序列.png b/pics/376.摆动序列.png new file mode 100644 index 00000000..c6a06dfe Binary files /dev/null and b/pics/376.摆动序列.png differ diff --git a/pics/376.摆动序列1.png b/pics/376.摆动序列1.png new file mode 100644 index 00000000..212e7722 Binary files /dev/null and b/pics/376.摆动序列1.png differ diff --git a/problems/0078.子集.md b/problems/0078.子集.md index 4987ed66..df7715c1 100644 --- a/problems/0078.子集.md +++ b/problems/0078.子集.md @@ -1,57 +1,74 @@ -# 题目地址 +> 认识本质之后,这就是一道模板题 # 第78题. 子集 + +题目地址:https://leetcode-cn.com/problems/subsets/ + 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 说明:解集不能包含重复的子集。 示例: - -输入: nums = [1,2,3] -输出: -[ - [3], -  [1], -  [2], -  [1,2,3], -  [1,3], -  [2,3], -  [1,2], -  [] -] - +输入: nums = [1,2,3] +输出: +[ + [3], +  [1], +  [2], +  [1,2,3], +  [1,3], +  [2,3], +  [1,2], +  [] +] # 思路 -求子集问题和 求组合组合和分割问题又不一样了, 如何把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是找树的叶子节点,而子集问题是找树的所有节点! +求子集问题和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:分割问题!](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)又不一样了。 -取子集也是,其实也是一种组合位置,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。 那么既然是无序,写回溯算法的时候,for就要从startIndex开始,而不是从0开始! +如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,**那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!** -那有同学问题,什么时候,for可以从0开始,求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合。 +其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。 + +**那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!** + +有同学问了,什么时候for可以从0开始呢? + +求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合,排列问题我们后续的文章就会讲到的。 以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下: -从图中,可以看出,遍历这个树的时候,把所有节点都记录下来,就是要求的子集。 +从图中红线部分,可以看出**遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合**。 -来看一下我总结的回溯模板来: +## 回溯三部曲 + +* 递归函数参数 + +全局变量数组path为子集收集元素,二维数组result存放子集组合。(也可以放到递归函数参数里) + +递归函数参数在上面讲到了,需要startIndex。 + +代码如下: ``` -backtracking() { - if (终止条件) { - 存放结果; - } - - for (选择:选择列表(可以想成树中节点孩子的数量)) { - 递归,处理节点; - backtracking(); - 回溯,撤销处理结果 - } -} +vector> result; +vector path; +void backtracking(vector& nums, int startIndex) { ``` -首先是终止条件,终止条件,就是startIndex已经大于数组的长度了,就是终止了,代码如下: +* 递归终止条件 + +从图中可以看出: + + + +剩余集合为空的时候,就是叶子节点。 + +那么什么时候剩余集合为空呢? + +就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,代码如下: ``` if (startIndex >= nums.size()) { @@ -59,35 +76,42 @@ if (startIndex >= nums.size()) { } ``` -但是,要明确的是,**求取子集问题,其实没有必要加终止条件,因为子集就是要遍历整个一棵树,不需要任何剪枝!** +**其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了**。 -大家一会看到下面整体代码的时候就知道了。 +* 单层搜索逻辑 -然后就是看如何写for循环,**因为求子集也是无序的,所以for循环要从startIndex开始!** +**求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树**。 -代码如下: +那么单层递归逻辑代码如下: ``` for (int i = startIndex; i < nums.size(); i++) { -``` - -接下来就是递归与回溯,定一个`vector path`,用来收集子集的元素,在回溯的时候还要弹出,backtracking每次调用自己的时候,记着要从i+1 开始,代码如下: - -``` -for (int i = startIndex; i < nums.size(); i++) { - path.push_back(nums[i]); - backtracking(nums, i + 1); - path.pop_back(); + path.push_back(nums[i]); // 子集收集元素 + backtracking(nums, i + 1); // 注意从i+1开始,元素不重复取 + path.pop_back(); // 回溯 } ``` -重点代码分析完之后,整体代码如下: +## C++代码 -可以发现我在backtracking里并没有写终止条件,因为本来我们就要遍历整颗树。 +根据[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)给出的回溯算法模板: -有的同学可能担心会不会无限递归? 并不会,因为每次递归的下一层就是从i+1开始的。 +``` +void backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } -# C++代码 + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} +``` + +可以写出如下回溯算法C++代码: ``` class Solution { @@ -95,7 +119,10 @@ private: vector> result; vector path; void backtracking(vector& nums, int startIndex) { - result.push_back(path); + result.push_back(path); // 收集子集 + if (startIndex >= nums.size()) { // 终止条件可以不加 + return; + } for (int i = startIndex; i < nums.size(); i++) { path.push_back(nums[i]); backtracking(nums, i + 1); @@ -110,4 +137,35 @@ public: return result; } }; + ``` + +在注释中,可以发现可以不写终止条件,因为本来我们就要遍历整颗树。 + +有的同学可能担心不写终止条件会不会无限递归? + +并不会,因为每次递归的下一层就是从i+1开始的。 + +# 总结 + +相信大家经过了 +* 组合问题: + * [回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ) + * [回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA) + * [回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) + * [回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) + * [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw) + * [回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) +* 分割问题: + * [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) + * [回溯算法:复原IP地址](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA) + +洗礼之后,发现子集问题还真的有点简单了,其实这就是一道标准的模板题。 + +但是要清楚子集问题和组合问题、分割问题的的区别,**子集是收集树形结构中树的所有节点的结果**。 + +**而组合问题、分割问题是收集树形结构中叶子节点的结果**。 + +**就酱,如果感觉收获满满,就帮Carl宣传一波「代码随想录」吧!** + + diff --git a/problems/0093.复原IP地址.md b/problems/0093.复原IP地址.md index ab6621d8..c7150033 100644 --- a/problems/0093.复原IP地址.md +++ b/problems/0093.复原IP地址.md @@ -223,6 +223,7 @@ private: public: vector restoreIpAddresses(string s) { result.clear(); + if (s.size() > 12) return result; // 算是剪枝了 backtracking(s, 0, 0); return result; } diff --git a/problems/0134.加油站.md b/problems/0134.加油站.md new file mode 100644 index 00000000..50872b48 --- /dev/null +++ b/problems/0134.加油站.md @@ -0,0 +1,71 @@ + +# 思路 + +这道题目贪心不好讲解啊 + +如果 get总和大于cost总和,那么一定是可以跑一圈的,因为油是可以存储的。 + +本题贪心的思路,不是那么好想。 + +那么来看一下贪心主要贪在哪里 + +* 如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的 +* remain[i] = gas[i]-cost[i]为一天剩下的油,remain[i],i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。 + +* 如果累加的最小值是负数,就要从非0节点出发,从后向前,看那个节点能这个负数填平。 + +这个方法太绝了 +``` +class Solution { +public: + int canCompleteCircuit(vector& gas, vector& cost) { + int curSum = 0; + int totalSum = 0; + int start = 0; + for (int i = 0; i < gas.size(); i++) { + curSum += gas[i] - cost[i]; + totalSum += gas[i] - cost[i]; + if (curSum < 0) { + start = i + 1; + curSum = 0; + } + } + if (totalSum < 0) return -1; + return start; + } +}; +``` + +这个方法太复杂了 +``` +class Solution { +public: + int canCompleteCircuit(vector& gas, vector& cost) { + int remainSum = 0; + int min = INT_MAX; // 从起点出发,油箱里的油量 + for (int i = 0; i < gas.size(); i++) { + int remain = gas[i] - cost[i]; + remainSum += remain; + if (remainSum < min) { + min = remainSum; + } + } + if (remainSum < 0) return -1; // 如果总油量-总消耗都小于零,一定是哪里作为起点都不行 + if (min >= 0) return 0; // 从0的位置出发,油箱里的油量没有出现负数,说明从0触发可以跑一圈 + // 否则就一定是从其他节点触发 + // 从后向前遍历,如果那个节点可以补上从0触发油箱出现负数的情况,那么这个i就是起点 + for (int i = gas.size() - 1; i >= 0; i--) { + int remain = gas[i] - cost[i]; + min += remain; + if (min >= 0) { + return i; + } + } + + return -1; + + } +}; +``` + + diff --git a/problems/0135.分发糖果.md b/problems/0135.分发糖果.md new file mode 100644 index 00000000..7a27091c --- /dev/null +++ b/problems/0135.分发糖果.md @@ -0,0 +1,21 @@ + +这道题目上来也是没什么思路啊 +``` +class Solution { +public: + int candy(vector& ratings) { + vector candyVec(ratings.size(), 1); + for (int i = 1; i < ratings.size(); i++) { + if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1; + } + for (int i = ratings.size() - 2; i >= 0; i--) { + if (ratings[i] > ratings[i + 1] && candyVec[i] < candyVec[i + 1] + 1) { + candyVec[i] = candyVec[i + 1] + 1; + } + } + int result = 0; + for (int i = 0; i < candyVec.size(); i++) result += candyVec[i]; + return result; + } +}; +``` diff --git a/problems/0376.摆动序列.md b/problems/0376.摆动序列.md new file mode 100644 index 00000000..dc799f3a --- /dev/null +++ b/problems/0376.摆动序列.md @@ -0,0 +1,56 @@ + +题目链接: https://leetcode-cn.com/problems/wiggle-subsequence/ + +## 思路 + +本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。 + +相信这么一说吓退不少同学,这又可以修改数组,这得如何修改呢? + +我们来分析一下,要求删除元素使其达到最大摆动序列,应该删除什么元素呢? + +用示例二来举例,如图所示: + + + +图中可以看出,为了让摆动序列最长,只需要把单一坡度(递增或者递减)上的节点删掉就可以了。 + +**这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点**。 + +**实际操作上,其实练删除的操作都不用做,因为题目要求的是摆动序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)** + +代码实现中,还有一些技巧,例如统计峰值的时候,数组最左面和最右面是最不好统计的。 + +例如数组[2,5],它的峰值数量是2,如果靠统计差值来计算峰值就需要考虑数组最左面和最右面的特殊情况。 + +所以可以针对数组[2,5],假设为[2,2,5],这样它就有坡度了,如图: + + + +这样result初始为1,curDiff > 0 && preDiff <= 0,result++,最后得到的result就是2了。 + +C++代码如下: + +``` +class Solution { +public: + int wiggleMaxLength(vector& nums) { + if (nums.size() <= 1) return nums.size(); + int curDiff = 0; // 当前一对差值 + int preDiff = 0; // 前一对差值 + int result = 1; // 记录峰值,起始位置峰值为1 + for (int i = 1; i < nums.size(); i++) { + curDiff = nums[i] - nums[i - 1]; + // 出现峰值 + if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) { + result++; + preDiff = curDiff; + } + } + return result; + } +}; +``` + +> 我是[程序员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/1356.根据数字二进制下1的数目排序.md b/problems/1356.根据数字二进制下1的数目排序.md new file mode 100644 index 00000000..6437eb39 --- /dev/null +++ b/problems/1356.根据数字二进制下1的数目排序.md @@ -0,0 +1,71 @@ + +## 题目链接 +https://leetcode-cn.com/problems/sort-integers-by-the-number-of-1-bits/ + +## 思路 + +这道题其实是考察如何计算一个数的二进制中1的数量。 + +我提供两种方法: + +* 方法一: + +朴实无华挨个计算1的数量,最多就是循环n的二进制位数,32位。 + +``` +int bitCount(int n) { + int count = 0; // 计数器 + while (n > 0) { + if((n & 1) == 1) count++; // 当前位是1,count++ + n >>= 1 ; // n向右移位 + } + return count; +} +``` + +* 方法二 + +这种方法,只循环n的二进制中1的个数次,比方法一高效的多 + +``` +int bitCount(int n) { + int count = 0; + while (n) { + n &= (n - 1); // 清除最低位的1 + count++; + } + return count; +} +``` +以计算12的二进制1的数量为例,如图所示: + + + +下面我就使用方法二,来做这道题目: + +## C++代码 + +``` +class Solution { +private: + static int bitCount(int n) { // 计算n的二进制中1的数量 + int count = 0; + while(n) { + n &= (n -1); // 清除最低位的1 + count++; + } + return count; + } + static bool cmp(int a, int b) { + int bitA = bitCount(a); + int bitB = bitCount(b); + if (bitA == bitB) return a < b; // 如果bit中1数量相同,比较数值大小 + return bitA < bitB; // 否则比较bit中1数量大小 + } +public: + vector sortByBits(vector& arr) { + sort(arr.begin(), arr.end(), cmp); + return arr; + } +}; +```