mirror of
https://github.com/youngyangyang04/leetcode-master.git
synced 2025-07-06 23:28:29 +08:00
Update
This commit is contained in:
BIN
pics/17. 电话号码的字母组合.jpeg
Normal file
BIN
pics/17. 电话号码的字母组合.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 115 KiB |
BIN
pics/491. 递增子序列2.png
Normal file
BIN
pics/491. 递增子序列2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
BIN
pics/491. 递增子序列3.png
Normal file
BIN
pics/491. 递增子序列3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
@ -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循环,以此类推,然后发现代码根本写不出来。
|
||||
|
||||
**遇到这种情况,就应该想到回溯了。**
|
||||
|
||||
这是一个回溯法的经典题目,**不要以为回溯是一个性能很高的算法,回溯其实就是暴力枚举,纯暴力,搜出所有的可能性。**
|
||||
|
||||
回溯一般都伴随着递归,而这种组合问题,都可以画成一个树形结构。如图所示:
|
||||
|
||||
<img src='../pics/17. 电话号码的字母组合.jpeg' width=600> </img></div>
|
||||
|
||||
可以想成遍历这棵树,然后把叶子节点都保存下来。
|
||||
|
||||
|
||||
3. 输入1 * #按键等等异常情况
|
||||
|
||||
题目的测试数据中应该没有异常情况的数据,可以不考虑,但是要知道会有这些异常。
|
||||
|
||||
|
||||
**那么在来讲一讲回溯法,回溯法的模板如下:**
|
||||
|
||||
```
|
||||
回溯函数() {
|
||||
if (终止条件) {
|
||||
存放结果;
|
||||
}
|
||||
|
||||
for (枚举同一个位置的所有可能性,可以想成节点孩子的数量) {
|
||||
递归,处理下一个孩子;
|
||||
(递归的下面就是回溯的过程);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
按照这个模板,不难写出如下代码:
|
||||
|
||||
## C++代码
|
||||
|
||||
|
@ -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
|
||||
|
||||
# 思路
|
||||
|
||||
这道题目其实是一道很简单的题,但是为什么通过率相对来说并不高呢,我理解是大家对 边界处理的判断有所失误,导致的。
|
||||
这道题目不难,但是为什么通过率相对来说并不高呢,我理解是大家对边界处理的判断有所失误导致的。
|
||||
|
||||
这道题目,我们要在数组中插入目标值,无非是这四种情况
|
||||
这道题目,要在数组中插入目标值,无非是这四种情况。
|
||||
|
||||
<img src='../pics/35_搜索插入位置3.png' width=600> </img></div>
|
||||
|
||||
@ -15,14 +39,15 @@ https://leetcode-cn.com/problems/search-insert-position/
|
||||
* 目标值插入数组中的位置
|
||||
* 目标值在数组所有元素之后
|
||||
|
||||
这四种情况确认清楚了,我们就可以尝试解题了
|
||||
这四种情况确认清楚了,就可以尝试解题了。
|
||||
|
||||
接下来我将从暴力的解法和二分法来讲解此题,也借此好好讲一讲二分查找法。
|
||||
|
||||
## 暴力解法
|
||||
|
||||
暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。
|
||||
|
||||
这里我给出了一种简洁的暴力解法,和两种二分查找的解法
|
||||
|
||||
|
||||
# 解法:暴力枚举
|
||||
## 暴力解法C++代码
|
||||
|
||||
```
|
||||
class Solution {
|
||||
@ -42,42 +67,47 @@ public:
|
||||
}
|
||||
};
|
||||
```
|
||||
效率如下:
|
||||
<img src='../pics/35_搜索插入位置.png' width=600> </img></div>
|
||||
|
||||
时间复杂度:O(n)
|
||||
时间复杂度:O(n)
|
||||
时间复杂度:O(1)
|
||||
|
||||
效率如下:
|
||||
|
||||
# 二分法
|
||||
<img src='../pics/35_搜索插入位置.png' width=600> </img></div>
|
||||
|
||||
既然暴力解法的时间复杂度是On,我们就要尝试一下使用二分查找法。
|
||||
## 二分法
|
||||
|
||||
既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。
|
||||
|
||||
<img src='../pics/35_搜索插入位置4.png' width=600> </img></div>
|
||||
|
||||
大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件
|
||||
大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件。
|
||||
|
||||
以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。**
|
||||
|
||||
同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下表可能不是唯一的。
|
||||
|
||||
大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,我们使用二分法寻找元素为5的位置,并返回其下标
|
||||
大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,使用二分法寻找元素为5的位置,并返回其下标。
|
||||
|
||||
<img src='../pics/35_搜索插入位置5.png' width=600> </img></div>
|
||||
|
||||
二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好
|
||||
二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好。
|
||||
|
||||
相信很多同学对二分查找法中边界条件处理不好,例如 到底是 小于 还是 小于等于, 到底是+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<int>& 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<int>& 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」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。
|
||||
|
||||
|
@ -34,7 +34,6 @@ public:
|
||||
int countNodes(TreeNode* root) {
|
||||
queue<TreeNode*> que;
|
||||
if (root != NULL) que.push(root);
|
||||
int count = 0;
|
||||
int result = 0;
|
||||
while (!que.empty()) {
|
||||
int size = que.size();
|
||||
|
@ -40,3 +40,49 @@ public:
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
一位师弟在评论中对代码进行了改进,效率确实高了很多,优化后如图:
|
||||
|
||||
<img src='../pics/491. 递增子序列2.png' width=600> </img></div>
|
||||
|
||||
改动的地方主要是将去重的逻辑中把 unordered_set 改成了 数组。
|
||||
|
||||
用数组替换unordered_set 确实可以快很多,unordered_set底层符号表也是哈希表,理论上不应该差多少。
|
||||
|
||||
估计程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)费了些时间。
|
||||
|
||||
用数组来做哈希,效率就高了很多,再加上题目中也说了,数值范围[-100,100],所以用数组正合适。
|
||||
|
||||
**这个事实告诉我们,使用哈希法的时候,条件允许的话,能用数组尽量用数组。**
|
||||
|
||||
优化后的代码如下:
|
||||
|
||||
```
|
||||
class Solution {
|
||||
private:
|
||||
void backtracking(vector<int>& nums, vector<vector<int>>& result, vector<int>& 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<vector<int>> findSubsequences(vector<int>& nums) {
|
||||
vector<vector<int>> result;
|
||||
vector<int> subseq;
|
||||
backtracking(nums, result, subseq, 0);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
Reference in New Issue
Block a user