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;
+ }
+};
+
+```