Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 96f7558

Browse files
Update
1 parent 6cc132a commit 96f7558

7 files changed

+155
-30
lines changed
115 KB
Loading

pics/491. 递增子序列2.png

42.8 KB
Loading

pics/491. 递增子序列3.png

43.1 KB
Loading

problems/0017.电话号码的字母组合.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,58 @@
11

22

33
## 题目地址
4+
https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/
45

56
## 思路
67

8+
本题要解决如下问题:
79

810
1. 数字和字母如何映射
911
2. 两个字母我就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来
10-
3. 如何排列组合呢
11-
4. 1 * # 等等情况
12+
3. 输入1 * #按键等等异常情况
1213

13-
树形结构啊
14+
接下来一一解决这几个问题。
15+
16+
17+
1. 数字和字母如何映射
18+
19+
定义一个二位数组,例如:string letterMap[10],来做映射
20+
21+
2. 两个字母我就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来。
22+
23+
**遇到这种情况,就应该想到回溯了。**
24+
25+
这是一个回溯法的经典题目,**不要以为回溯是一个性能很高的算法,回溯其实就是暴力枚举,纯暴力,搜出所有的可能性。**
26+
27+
回溯一般都伴随着递归,而这种组合问题,都可以画成一个树形结构。如图所示:
28+
29+
<img src='../pics/17. 电话号码的字母组合.jpeg' width=600> </img></div>
30+
31+
可以想成遍历这棵树,然后把叶子节点都保存下来。
32+
33+
34+
3. 输入1 * #按键等等异常情况
35+
36+
题目的测试数据中应该没有异常情况的数据,可以不考虑,但是要知道会有这些异常。
37+
38+
39+
**那么在来讲一讲回溯法,回溯法的模板如下:**
40+
41+
```
42+
回溯函数() {
43+
if (终止条件) {
44+
存放结果;
45+
}
46+
47+
for (枚举同一个位置的所有可能性,可以想成节点孩子的数量) {
48+
递归,处理下一个孩子;
49+
(递归的下面就是回溯的过程);
50+
}
51+
}
52+
53+
```
54+
55+
按照这个模板,不难写出如下代码:
1456

1557
## C++代码
1658

problems/0035.搜索插入位置.md

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,35 @@
22

33
https://leetcode-cn.com/problems/search-insert-position/
44

5+
> 二分查找法是数组里的常用方法,彻底掌握它是十分必要的。
6+
7+
# 编号35:搜索插入位置
8+
9+
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
10+
11+
你可以假设数组中无重复元素。
12+
13+
示例 1:
14+
输入: [1,3,5,6], 5
15+
输出: 2
16+
17+
示例 2:
18+
输入: [1,3,5,6], 2
19+
输出: 1
20+
21+
示例 3:
22+
输入: [1,3,5,6], 7
23+
输出: 4
24+
25+
示例 4:
26+
输入: [1,3,5,6], 0
27+
输出: 0
28+
529
# 思路
630

7-
这道题目其实是一道很简单的题,但是为什么通过率相对来说并不高呢,我理解是大家对 边界处理的判断有所失误,导致的
31+
这道题目不难,但是为什么通过率相对来说并不高呢,我理解是大家对边界处理的判断有所失误导致的
832

9-
这道题目,我们要在数组中插入目标值,无非是这四种情况
33+
这道题目,要在数组中插入目标值,无非是这四种情况
1034

1135
<img src='../pics/35_搜索插入位置3.png' width=600> </img></div>
1236

@@ -15,14 +39,15 @@ https://leetcode-cn.com/problems/search-insert-position/
1539
* 目标值插入数组中的位置
1640
* 目标值在数组所有元素之后
1741

18-
这四种情况确认清楚了,我们就可以尝试解题了
42+
这四种情况确认清楚了,就可以尝试解题了。
1943

20-
暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的
44+
接下来我将从暴力的解法和二分法来讲解此题,也借此好好讲一讲二分查找法
2145

22-
这里我给出了一种简洁的暴力解法,和两种二分查找的解法
46+
## 暴力解法
2347

48+
暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。
2449

25-
# 解法:暴力枚举
50+
## 暴力解法C++代码
2651

2752
```
2853
class Solution {
@@ -42,50 +67,55 @@ public:
4267
}
4368
};
4469
```
45-
效率如下:
46-
<img src='../pics/35_搜索插入位置.png' width=600> </img></div>
4770

48-
时间复杂度:O(n)
71+
时间复杂度:O(n)
4972
时间复杂度:O(1)
5073

74+
效率如下:
75+
76+
<img src='../pics/35_搜索插入位置.png' width=600> </img></div>
5177

52-
# 二分法
78+
## 二分法
5379

54-
既然暴力解法的时间复杂度是On,我们就要尝试一下使用二分查找法
80+
既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法
5581

5682
<img src='../pics/35_搜索插入位置4.png' width=600> </img></div>
5783

58-
大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件
84+
大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件
5985

6086
以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。**
6187

6288
同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下表可能不是唯一的。
6389

64-
大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,我们使用二分法寻找元素为5的位置,并返回其下标
90+
大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,使用二分法寻找元素为5的位置,并返回其下标
6591

6692
<img src='../pics/35_搜索插入位置5.png' width=600> </img></div>
6793

68-
二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好
94+
二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好。
95+
96+
相信很多同学对二分查找法中边界条件处理不好。
6997

70-
相信很多同学对二分查找法中边界条件处理不好,例如 到底是 小于 还是 小于等于, 到底是+1 呢,还是要-1呢
98+
例如到底是 `while(left < right)` 还是 `while(left <= right)`到底是`right = middle`呢,还是要`right = middle - 1`呢?
7199

72-
这是为什么呢,主要是**我们对区间的定义没有想清楚,这就是我们的不变量**
100+
这里弄不清楚主要是因为**对区间的定义没有想清楚,这就是不变量**
73101

74-
我们要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查)
102+
要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查)
75103

76104
## 二分法第一种写法
77105

78-
以这道题目来举例,以下的代码中我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right]
106+
以这道题目来举例,以下的代码中定义 target 是在一个在左闭右闭的区间里,**也就是[left, right] (这个很重要)**
107+
108+
这就决定了这个二分法的代码如何去写,大家看如下代码:
79109

80-
这就决定了我们 这个二分法的代码如何去写,大家看如下代码
110+
**大家要仔细看注释,思考为什么要写while(left <= right), 为什么要写right = middle - 1**
81111

82112
```
83113
class Solution {
84114
public:
85115
int searchInsert(vector<int>& nums, int target) {
86116
int n = nums.size();
87117
int left = 0;
88-
int right = n - 1; // 我们定义target在左闭右闭的区间里,[left, right]
118+
int right = n - 1; // 定义target在左闭右闭的区间里,[left, right]
89119
while (left <= right) { // 当left==right,区间[left, right]依然有效
90120
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
91121
if (nums[middle] > target) {
@@ -105,27 +135,29 @@ public:
105135
}
106136
};
107137
```
108-
时间复杂度:O(logn)
138+
时间复杂度:O(logn)
109139
时间复杂度:O(1)
110140

111141
效率如下:
112142
<img src='../pics/35_搜索插入位置2.png' width=600> </img></div>
113143

114144
## 二分法第二种写法
115145

116-
如果说我们定义 target 是在一个在左闭右开的区间里,也就是[left, right)
146+
如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right)
117147

118148
那么二分法的边界处理方式则截然不同。
119149

120150
不变量是[left, right)的区间,如下代码可以看出是如何在循环中坚持不变量的。
121151

152+
**大家要仔细看注释,思考为什么要写while (left < right), 为什么要写right = middle**
153+
122154
```
123155
class Solution {
124156
public:
125157
int searchInsert(vector<int>& nums, int target) {
126158
int n = nums.size();
127159
int left = 0;
128-
int right = n; // 我们定义target在左闭右开的区间里,[left, right) target
160+
int right = n; // 定义target在左闭右开的区间里,[left, right) target
129161
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间
130162
int middle = left + ((right - left) >> 1);
131163
if (nums[middle] > target) {
@@ -146,11 +178,17 @@ public:
146178
};
147179
```
148180

149-
时间复杂度:O(logn)
181+
时间复杂度:O(logn)
150182
时间复杂度:O(1)
151183

152-
## 总结
153-
希望通过这道题目 ,可以帮助大家对二分法有更深的理解
184+
# 总结
185+
186+
希望通过这道题目,大家会发现平时写二分法,为什么总写不好,就是因为对区间定义不清楚。
187+
188+
确定要查找的区间到底是左闭右开[left, right),还是左闭又闭[left, right],这就是不变量。
189+
190+
然后在**二分查找的循环中,坚持循环不变量的原则**,很多细节问题,自然会知道如何处理了。
191+
154192

155193
> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。
156194

problems/0222.完全二叉树的节点个数.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ public:
3434
int countNodes(TreeNode* root) {
3535
queue<TreeNode*> que;
3636
if (root != NULL) que.push(root);
37-
int count = 0;
3837
int result = 0;
3938
while (!que.empty()) {
4039
int size = que.size();

problems/0491.递增子序列.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,49 @@ public:
4040
}
4141
};
4242
```
43+
44+
一位师弟在评论中对代码进行了改进,效率确实高了很多,优化后如图:
45+
46+
<img src='../pics/491. 递增子序列2.png' width=600> </img></div>
47+
48+
改动的地方主要是将去重的逻辑中把 unordered_set 改成了 数组。
49+
50+
用数组替换unordered_set 确实可以快很多,unordered_set底层符号表也是哈希表,理论上不应该差多少。
51+
52+
估计程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)费了些时间。
53+
54+
用数组来做哈希,效率就高了很多,再加上题目中也说了,数值范围[-100,100],所以用数组正合适。
55+
56+
**这个事实告诉我们,使用哈希法的时候,条件允许的话,能用数组尽量用数组。**
57+
58+
优化后的代码如下:
59+
60+
```
61+
class Solution {
62+
private:
63+
void backtracking(vector<int>& nums, vector<vector<int>>& result, vector<int>& subseq, int startIndex) {
64+
if (subseq.size() > 1) {
65+
result.push_back(subseq);
66+
// 注意这里不要加return,因为要取所有的可能
67+
}
68+
int hash[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100]
69+
for (int i = startIndex; i < nums.size(); i++) {
70+
if ((subseq.empty() || nums[i] >= subseq.back())
71+
&& hash[nums[i] + 100] == 0) {
72+
subseq.push_back(nums[i]);
73+
backtracking(nums, result, subseq, i + 1);
74+
subseq.pop_back();
75+
hash[nums[i]+100] = 1;
76+
}
77+
}
78+
}
79+
public:
80+
vector<vector<int>> findSubsequences(vector<int>& nums) {
81+
vector<vector<int>> result;
82+
vector<int> subseq;
83+
backtracking(nums, result, subseq, 0);
84+
return result;
85+
}
86+
};
87+
88+
```

0 commit comments

Comments
 (0)