Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
youngyangyang04 committed Aug 26, 2020
1 parent 6cc132a commit 96f7558
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 30 deletions.
Binary file added pics/17. 电话号码的字母组合.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pics/491. 递增子序列2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pics/491. 递增子序列3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 45 additions & 3 deletions problems/0017.电话号码的字母组合.md
Original file line number Diff line number Diff line change
@@ -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++代码

Expand Down
90 changes: 64 additions & 26 deletions problems/0035.搜索插入位置.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>

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

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

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

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

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

# 解法:暴力枚举
## 暴力解法C++代码

```
class Solution {
Expand All @@ -42,50 +67,55 @@ 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 {
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) {
Expand All @@ -105,27 +135,29 @@ public:
}
};
```
时间复杂度:O(logn)
时间复杂度:O(logn)
时间复杂度:O(1)

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

## 二分法第二种写法

如果说我们定义 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) {
Expand All @@ -146,11 +178,17 @@ public:
};
```

时间复杂度:O(logn)
时间复杂度:O(logn)
时间复杂度:O(1)

## 总结
希望通过这道题目 ,可以帮助大家对二分法有更深的理解
# 总结

希望通过这道题目,大家会发现平时写二分法,为什么总写不好,就是因为对区间定义不清楚。

确定要查找的区间到底是左闭右开[left, right),还是左闭又闭[left, right],这就是不变量。

然后在**二分查找的循环中,坚持循环不变量的原则**,很多细节问题,自然会知道如何处理了。


> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。
1 change: 0 additions & 1 deletion problems/0222.完全二叉树的节点个数.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
46 changes: 46 additions & 0 deletions problems/0491.递增子序列.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};
```

0 comments on commit 96f7558

Please sign in to comment.