diff --git a/pics/17. 电话号码的字母组合.jpeg b/pics/17. 电话号码的字母组合.jpeg new file mode 100644 index 00000000..f80ec767 Binary files /dev/null and b/pics/17. 电话号码的字母组合.jpeg differ diff --git a/pics/491. 递增子序列2.png b/pics/491. 递增子序列2.png new file mode 100644 index 00000000..24988010 Binary files /dev/null and b/pics/491. 递增子序列2.png differ diff --git a/pics/491. 递增子序列3.png b/pics/491. 递增子序列3.png new file mode 100644 index 00000000..df293dd5 Binary files /dev/null and b/pics/491. 递增子序列3.png differ diff --git a/problems/0017.电话号码的字母组合.md b/problems/0017.电话号码的字母组合.md index bcf53662..e4257e24 100644 --- a/problems/0017.电话号码的字母组合.md +++ b/problems/0017.电话号码的字母组合.md @@ -1,16 +1,58 @@ ## 题目地址 +https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/ ## 思路 +本题要解决如下问题: 1. 数字和字母如何映射 2. 两个字母我就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来 -3. 如何排列组合呢 -4. 1 * # 等等情况 +3. 输入1 * #按键等等异常情况 -树形结构啊 +接下来一一解决这几个问题。 + + +1. 数字和字母如何映射 + +定义一个二位数组,例如:string letterMap[10],来做映射 + +2. 两个字母我就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来。 + +**遇到这种情况,就应该想到回溯了。** + +这是一个回溯法的经典题目,**不要以为回溯是一个性能很高的算法,回溯其实就是暴力枚举,纯暴力,搜出所有的可能性。** + +回溯一般都伴随着递归,而这种组合问题,都可以画成一个树形结构。如图所示: + + + +可以想成遍历这棵树,然后把叶子节点都保存下来。 + + +3. 输入1 * #按键等等异常情况 + +题目的测试数据中应该没有异常情况的数据,可以不考虑,但是要知道会有这些异常。 + + +**那么在来讲一讲回溯法,回溯法的模板如下:** + +``` +回溯函数() { + if (终止条件) { + 存放结果; + } + + for (枚举同一个位置的所有可能性,可以想成节点孩子的数量) { + 递归,处理下一个孩子; + (递归的下面就是回溯的过程); + } +} + +``` + +按照这个模板,不难写出如下代码: ## C++代码 diff --git a/problems/0035.搜索插入位置.md b/problems/0035.搜索插入位置.md index 328a6b65..64cb1075 100644 --- a/problems/0035.搜索插入位置.md +++ b/problems/0035.搜索插入位置.md @@ -2,11 +2,35 @@ https://leetcode-cn.com/problems/search-insert-position/ +> 二分查找法是数组里的常用方法,彻底掌握它是十分必要的。 + +# 编号35:搜索插入位置 + +给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 + +你可以假设数组中无重复元素。 + +示例 1: +输入: [1,3,5,6], 5 +输出: 2 + +示例 2: +输入: [1,3,5,6], 2 +输出: 1 + +示例 3: +输入: [1,3,5,6], 7 +输出: 4 + +示例 4: +输入: [1,3,5,6], 0 +输出: 0 + # 思路 -这道题目其实是一道很简单的题,但是为什么通过率相对来说并不高呢,我理解是大家对 边界处理的判断有所失误,导致的。 +这道题目不难,但是为什么通过率相对来说并不高呢,我理解是大家对边界处理的判断有所失误导致的。 -这道题目,我们要在数组中插入目标值,无非是这四种情况 +这道题目,要在数组中插入目标值,无非是这四种情况。 @@ -15,14 +39,15 @@ https://leetcode-cn.com/problems/search-insert-position/ * 目标值插入数组中的位置 * 目标值在数组所有元素之后 -这四种情况确认清楚了,我们就可以尝试解题了 +这四种情况确认清楚了,就可以尝试解题了。 + +接下来我将从暴力的解法和二分法来讲解此题,也借此好好讲一讲二分查找法。 + +## 暴力解法 暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。 -这里我给出了一种简洁的暴力解法,和两种二分查找的解法 - - -# 解法:暴力枚举 +## 暴力解法C++代码 ``` class Solution { @@ -42,42 +67,47 @@ public: } }; ``` -效率如下: - -时间复杂度:O(n) +时间复杂度:O(n) 时间复杂度:O(1) +效率如下: -# 二分法 + -既然暴力解法的时间复杂度是On,我们就要尝试一下使用二分查找法。 +## 二分法 + +既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。 -大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件 +大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件。 以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。** 同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下表可能不是唯一的。 -大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,我们使用二分法寻找元素为5的位置,并返回其下标 +大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,使用二分法寻找元素为5的位置,并返回其下标。 -二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好 +二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好。 -相信很多同学对二分查找法中边界条件处理不好,例如 到底是 小于 还是 小于等于, 到底是+1 呢,还是要-1呢 +相信很多同学对二分查找法中边界条件处理不好。 -这是为什么呢,主要是**我们对区间的定义没有想清楚,这就是我们的不变量** +例如到底是 `while(left < right)` 还是 `while(left <= right)`,到底是`right = middle`呢,还是要`right = middle - 1`呢? -我们要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查) +这里弄不清楚主要是因为**对区间的定义没有想清楚,这就是不变量**。 + +要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查)。 ## 二分法第一种写法 -以这道题目来举例,以下的代码中我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] +以这道题目来举例,以下的代码中定义 target 是在一个在左闭右闭的区间里,**也就是[left, right] (这个很重要)**。 -这就决定了我们 这个二分法的代码如何去写,大家看如下代码 +这就决定了这个二分法的代码如何去写,大家看如下代码: + +**大家要仔细看注释,思考为什么要写while(left <= right), 为什么要写right = middle - 1**。 ``` class Solution { @@ -85,7 +115,7 @@ public: int searchInsert(vector& nums, int target) { int n = nums.size(); int left = 0; - int right = n - 1; // 我们定义target在左闭右闭的区间里,[left, right] + int right = n - 1; // 定义target在左闭右闭的区间里,[left, right] while (left <= right) { // 当left==right,区间[left, right]依然有效 int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2 if (nums[middle] > target) { @@ -105,7 +135,7 @@ public: } }; ``` -时间复杂度:O(logn) +时间复杂度:O(logn) 时间复杂度:O(1) 效率如下: @@ -113,19 +143,21 @@ public: ## 二分法第二种写法 -如果说我们定义 target 是在一个在左闭右开的区间里,也就是[left, right) +如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) 。 那么二分法的边界处理方式则截然不同。 不变量是[left, right)的区间,如下代码可以看出是如何在循环中坚持不变量的。 +**大家要仔细看注释,思考为什么要写while (left < right), 为什么要写right = middle**。 + ``` class Solution { public: int searchInsert(vector& nums, int target) { int n = nums.size(); int left = 0; - int right = n; // 我们定义target在左闭右开的区间里,[left, right) target + int right = n; // 定义target在左闭右开的区间里,[left, right) target while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间 int middle = left + ((right - left) >> 1); if (nums[middle] > target) { @@ -146,11 +178,17 @@ public: }; ``` -时间复杂度:O(logn) +时间复杂度:O(logn) 时间复杂度:O(1) -## 总结 -希望通过这道题目 ,可以帮助大家对二分法有更深的理解 +# 总结 + +希望通过这道题目,大家会发现平时写二分法,为什么总写不好,就是因为对区间定义不清楚。 + +确定要查找的区间到底是左闭右开[left, right),还是左闭又闭[left, right],这就是不变量。 + +然后在**二分查找的循环中,坚持循环不变量的原则**,很多细节问题,自然会知道如何处理了。 + > 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0222.完全二叉树的节点个数.md b/problems/0222.完全二叉树的节点个数.md index 82a1627e..098d0edd 100644 --- a/problems/0222.完全二叉树的节点个数.md +++ b/problems/0222.完全二叉树的节点个数.md @@ -34,7 +34,6 @@ public: int countNodes(TreeNode* root) { queue que; if (root != NULL) que.push(root); - int count = 0; int result = 0; while (!que.empty()) { int size = que.size(); diff --git a/problems/0491.递增子序列.md b/problems/0491.递增子序列.md index 1aa8efc8..dd9d96b3 100644 --- a/problems/0491.递增子序列.md +++ b/problems/0491.递增子序列.md @@ -40,3 +40,49 @@ public: } }; ``` + +一位师弟在评论中对代码进行了改进,效率确实高了很多,优化后如图: + + + +改动的地方主要是将去重的逻辑中把 unordered_set 改成了 数组。 + +用数组替换unordered_set 确实可以快很多,unordered_set底层符号表也是哈希表,理论上不应该差多少。 + +估计程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)费了些时间。 + +用数组来做哈希,效率就高了很多,再加上题目中也说了,数值范围[-100,100],所以用数组正合适。 + +**这个事实告诉我们,使用哈希法的时候,条件允许的话,能用数组尽量用数组。** + +优化后的代码如下: + +``` +class Solution { +private: +void backtracking(vector& nums, vector>& result, vector& subseq, int startIndex) { + if (subseq.size() > 1) { + result.push_back(subseq); + // 注意这里不要加return,因为要取所有的可能 + } + int hash[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100] + for (int i = startIndex; i < nums.size(); i++) { + if ((subseq.empty() || nums[i] >= subseq.back()) + && hash[nums[i] + 100] == 0) { + subseq.push_back(nums[i]); + backtracking(nums, result, subseq, i + 1); + subseq.pop_back(); + hash[nums[i]+100] = 1; + } + } +} +public: + vector> findSubsequences(vector& nums) { + vector> result; + vector subseq; + backtracking(nums, result, subseq, 0); + return result; + } +}; + +```