This commit is contained in:
youngyangyang04
2020-05-18 00:28:44 +08:00
parent bd74495419
commit 2c4233b522
18 changed files with 488 additions and 0 deletions

View File

@ -0,0 +1,26 @@
## 题目地址
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/
## 思路
此题就是O(n)的解法,拼速度的话,也就是剪剪枝
注意题目中:你不需要考虑数组中超出新长度后面的元素。 说明是要对原数组进行操作的
## 解法
```
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0; // 别忘记空数组的判断
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < (nums.size() - 1); fastIndex++){
if(nums[fastIndex] != nums[fastIndex + 1]) { // 发现和后一个不相同
nums[++slowIndex] = nums[fastIndex + 1]; //slowIndex = 0 的数据一定是不重复的,所以直接 ++slowIndex
}
}
return slowIndex + 1; //别忘了slowIndex是从0开始的所以返回slowIndex + 1
}
};
```

View File

@ -0,0 +1,49 @@
> 笔者在BAT从事技术研发多年利用工作之余重刷leetcode更多原创文章请关注公众号「代码随想录」。
## 题目地址
https://leetcode-cn.com/problems/remove-element/
建议做完这道题,接着再去做 26. 删除排序数组中的重复项, 对这种类型的题目就有有所感觉
## 暴力解法
时间复杂度O(n^2)
空间复杂度O(1)
```
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 因为下表i以后的数值都向前移动了一位所以i也向前移动一位
size--;// 此时数组的大小-1
}
}
return size;
}
};
```
## 快慢指针解法
时间复杂度O(n)
空间复杂度O(1)
```
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0; // index为 慢指针
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { // i 为快指针
if (val != nums[fastIndex]) { //将快指针对应的数值赋值给慢指针对应的数值
nums[slowIndex++] = nums[fastIndex]; 注意这里是slowIndex++ 而不是slowIndex--
}
}
return slowIndex;
}
};
```

View File

@ -0,0 +1,156 @@
## 题目地址
https://leetcode-cn.com/problems/search-insert-position/
## 思路
这道题目其实是一道很简单的题,但是为什么通过率相对来说并不高呢,我理解是大家对 边界处理的判断有所失误,导致的。
这道题目,我们要在数组中插入目标值,无非是这四种情况
<img src='../pics/35_搜索插入位置3.png' width=600> </img></div>
* 目标值在数组所有元素之前
* 目标值等于数组中某一个元素
* 目标值插入数组中的位置
* 目标值在数组所有元素之后
这四种情况确认清楚了,我们就可以尝试解题了
暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。
这里我给出了一种简洁的暴力解法,和两种二分查找的解法
## 解法:暴力枚举
```
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
for (int i = 0; i < nums.size(); i++) {
// 分别处理如下三种情况
// 目标值在数组所有元素之前
// 目标值等于数组中某一个元素
// 目标值插入数组中的位置
if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i]那么i就是我们要的结果
return i;
}
}
// 目标值在数组所有元素之后的情况
return nums.size(); // 如果target是最大的或者 nums为空则返回nums的长度
}
};
```
效率如下:
<img src='../pics/35_搜索插入位置.png' width=600> </img></div>
时间复杂度O(n)
时间复杂度O(1)
## 二分法
既然暴力解法的时间复杂度是On我们就要尝试一下使用二分查找法。
<img src='../pics/35_搜索插入位置4.png' width=600> </img></div>
大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件
以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。**
同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下表可能不是唯一的。
大体讲解一下二分法的思路这里来举一个例子例如在这个数组中我们使用二分法寻找元素为5的位置并返回其下标
<img src='../pics/35_搜索插入位置5.png' width=600> </img></div>
二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好
相信很多同学对二分查找法中边界条件处理不好,例如 到底是 小于 还是 小于等于, 到底是+1 呢,还是要-1呢
这是为什么呢,主要是**我们对区间的定义没有想清楚,这就是我们的不变量**
我们要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查)
### 二分法第一种写法
以这道题目来举例,以下的代码中我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right]
这就决定了我们 这个二分法的代码如何去写,大家看如下代码
```
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
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) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle;
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0, -1]
// 目标值等于数组中某一个元素 return middle;
// 目标值插入数组中的位置 [left, right]return right + 1
// 目标值在数组所有元素之后的情况 [left, right] return right + 1
return right + 1;
}
};
```
时间复杂度O(logn)
时间复杂度O(1)
效率如下:
<img src='../pics/35_搜索插入位置2.png' width=600> </img></div>
### 二分法第二种写法
如果说我们定义 target 是在一个在左闭右开的区间里,也就是[left, right)
那么二分法的边界处理方式则截然不同。
不变量是[left, right)的区间,如下代码可以看出是如何在循环中坚持不变量的。
```
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n; // 我们定义target在左闭右开的区间里[left, right) target
while (left < right) { // 因为left == right的时候在[left, right)是无效的空间
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在 [middle+1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0,0)
// 目标值等于数组中某一个元素 return middle
// 目标值插入数组中的位置 [left, right) return right 即可
// 目标值在数组所有元素之后的情况 [left, right)return right 即可
return right;
}
};
```
时间复杂度O(logn)
时间复杂度O(1)
## 总结
希望通过这道题目 ,可以帮助大家对二分法有更深的理解
> 笔者在BAT从事技术研发多年利用工作之余重刷leetcode更多原创文章请关注公众号「代码随想录」。

View File

@ -0,0 +1,55 @@
> 笔者在BAT从事技术研发多年利用工作之余重刷leetcode更多原创技术文章欢迎关注「代码随想录」校招社招求职内推欢迎通过公众号「代码随想录」联系我度厂很缺人
> 笔者在BAT从事技术研发多年利用工作之余重刷leetcode希望结合自己多年的实践经验把算法讲的更清楚更多原创文章欢迎关注公众号「代码随想录」。
## 题目地址
https://leetcode-cn.com/problems/maximum-subarray/
## 暴力解法
暴力解法的思路第一层for 就是设置起始位置第二层for循环遍历数组寻找最大值
时间复杂度O(n^2)
空间复杂度O(1)
```
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT32_MIN;
int count = 0;
for (int i = 0; i < nums.size(); i++) { // 设置起始位置
count = 0;
for (int j = i; j < nums.size(); j++) { // 每次从起始位置i开始遍历寻找最大值
count += nums[j];
result = count > result ? count : result;
}
}
return result;
}
};
```
## 贪心解法
贪心解法,其实不是很好理解, 看上面暴力的解法是两层for循环那如何省掉一层for循环呢
其实**暴力解法中设置起始位置这个for循环是可以省略掉的**,这也是关键的一步,有了这个思路之后,就可以看一下代码了
时间复杂度O(n)
空间复杂度O(1)
```
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT32_MIN;
int count = 0;
for (int i = 0; i < nums.size(); i++) {
count += nums[i];
if (count > result) { // 相当于每次取各个终点的最大值
result = count;
}
if (count <= 0) count = 0; // 相当于重置起点,因为遇到负数一定是拉低总体数值的
}
return result;
}
};
```

View File

@ -0,0 +1,73 @@
> 笔者在BAT从事技术研发多年利用工作之余重刷leetcode更多原创文章请关注公众号「代码随想录」。
## 题目地址
https://leetcode-cn.com/problems/spiral-matrix-ii/
## 思路
模拟顺时针画矩阵的过程
填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上
在模拟的过程中最重要的思想是**保持画每一条边的原则一致,即每次都是左闭右开的原则**,如果所示
<img src='../pics/螺旋矩阵.png' width=600> </img></div>
很多同学,做这道题目之所以一直写不好,代码越写越乱,就是因为 在画每一条边的时候,没有保证统一原则
例如:模拟矩阵上边的时候 左闭右开,然后模拟矩阵右列的时候又开始了左闭又闭,那岂能不乱
## 解法
```C++
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次
int mid = n / 2; // 矩阵中间的位置例如n为3 中间的位置就是(11)n为5中间位置为(3, 3)
int count = 1; // 用来计数
int offset = 1; // 每一圈循环,需要偏移的位置
int i,j;
while (loop --) {
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j = starty; j < starty + n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < startx + n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候起始位置要各自加1 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈,遍历的长度
offset += 2;
}
// 如果n为奇数的话需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};
```

View File

@ -0,0 +1,62 @@
## 题目地址
https://leetcode-cn.com/problems/minimum-size-subarray-sum/
## 思路
这道题目 暴力解法当然是 两个for循环然后不断的寻找符合条件的子序列
这块我们还可以使用滑动窗口的细想来做这道题。所谓滑动窗口,**就是不断的调节子序列的起始位置,从而得出我们要想的结果**
## 暴力解法
```
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
sum += nums[j];
if (sum >= s) { // 一旦发现子序列和超过了s更新result
subLength = j - i + 1; // 取子序列的长度
// result取 result和subLength最小的那个
result = result < subLength ? result : subLength;
break; // 因为我们是找符合条件最短的子序列所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话就返回0说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
```
## 滑动窗口
<img src='../pics/leetcode_209.png' width=600> </img></div>
```
// 滑动窗口
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while每次更新 i起始位置并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
// result取 result和subLength最小的那个
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处不断变更i子序列的起始位置
}
}
// 如果result没有被赋值的话就返回0说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
```

View File

@ -0,0 +1,67 @@
## 题目地址
https://leetcode-cn.com/problems/ransom-note/
## 思路
这道题题意很清晰就是用判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成但是这里需要注意两点1.
*  第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思”  这里*说明杂志里面的字母不可重复使用。*
* 第二点 “你可以假设两个字符串均只含有小写字母。” *说明只有小写字母*,这一点很重要
## 一般解法
那么第一个思路其实就是暴力枚举了两层for循环不断去寻找代码如下
```C++
// 时间复杂度: O(n^2)
// 空间复杂度O(1)
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
for (int i = 0; i < magazine.length(); i++) {
for (int j = 0; j < ransomNote.length(); j++) {
if (magazine[i] == ransomNote[j]) {
ransomNote.erase(ransomNote.begin() + j);
break;
}
}
}
if (ransomNote.length() == 0) {
return true;
}
return false;
}
};
```
这里时间复杂度是比较高的而且里面还有一个字符串删除也就是erase的操作也是费时的当然这段代码也可以过这道题
我们想一想优化解法
## 优化解法
因为题目所只有小写字母,那我们可以采用空间换区时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
代码如下:
```C++
// 时间复杂度: O(n)
// 空间复杂度O(1)
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
for (int i = 0; i < magazine.length(); i++) {
record[magazine[i]-'a'] ++;
}
for (int j = 0; j < ransomNote.length(); j++) {
record[ransomNote[j]-'a']--;
if(record[ransomNote[j]-'a'] < 0) {
return false;
}
}
return true;
}
};
```