This commit is contained in:
youngyangyang04
2020-08-27 16:27:41 +08:00
parent 6741e45afe
commit cfe3b6d2e4
4 changed files with 195 additions and 74 deletions

View File

@ -4,25 +4,50 @@
https://leetcode-cn.com/problems/remove-element/
> 移除元素想要高效的话,不是很简单!
# 编号27. 移除元素
给你一个数组 nums 和一个值 val你需要 原地 移除所有数值等于 val 的元素并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并**原地**修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
**你不需要考虑数组中超出新长度后面的元素。**
# 思路
有的同学可能说了,多余的元素,删掉不就得了。
**要清楚数组的元素在内存地址中是连续的,数组中的元素是不能删掉的,只能覆盖。**
**要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。**
数组的基础知识可以看这里[程序员算法面试中,必须掌握的数组理论知识](https://mp.weixin.qq.com/s/X7R55wSENyY62le0Fiawsg)。
# 暴力解法
这个题目暴力的解法就是两层for循环一个for循环遍历数组元素 第二个for循环更新数组。
如动画所示
删除过程如下
<video src='../video/27.移除元素-暴力解法.mp4' controls='controls' width='640' height='320' autoplay='autoplay'> Your browser does not support the video tag.</video></div>
<img src='../../World/Writing/leetcode/video/27.移除元素-暴力解法.gif' width=600> </img></div>
很明显暴力解法的时间复杂度是O(n^2)这道题目暴力解法在leetcode上是可以过的。
代码如下:
# 暴力解法C++代码
```
// 时间复杂度O(n^2)
// 空间复杂度O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
@ -33,7 +58,7 @@ public:
nums[j - 1] = nums[j];
}
i--; // 因为下表i以后的数值都向前移动了一位所以i也向前移动一位
size--;// 此时数组的大小-1
size--; // 此时数组的大小-1
}
}
return size;
@ -44,24 +69,42 @@ public:
# 双指针法
双指针法(快慢指针法): **说白了就是通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。**
双指针法(快慢指针法): **通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。**
先看一下动画理解一下:
删除过程如下:
<img src='../../World/Writing/leetcode/video/27.移除元素-双指针法.gif' width=600> </img></div>
<video src='../video/27.移除元素.mp4' controls='controls' width='640' height='320' autoplay='autoplay'> Your browser does not support the video tag.</video></div>
**双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。**
代码如下:
我们来回顾一下,之前已经讲过有四道题目使用了双指针法。
双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,题目如下:
* [15.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)
* [18.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g)
双指针来记录前后指针实现链表反转:
* [206.反转链表](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg)
使用双指针来确定有环:
* [142题.环形链表II](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)
双指针法在数组和链表中还有很多应用,后面还会介绍到。
# 双指针法C++代码:
```
// 时间复杂度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--
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
@ -69,62 +112,5 @@ public:
};
```
其实**双指针法快慢指针法在在数组和链表的操作中是非常常见的很多考察数组和链表操作的面试题都使用双指针法可以将时间复杂度O(n^2)的解法优化为 O(n)的解法,例如:**
* [0015.三数之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0015.三数之和.md)
* [0018.四数之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0018.四数之和.md)
* [0026.删除排序数组中的重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/0026.删除排序数组中的重复项.md)
* [0206.翻转链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0206.翻转链表.md)
* [0344.反转字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0344.反转字符串.md)
* [剑指Offer05.替换空格](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer05.替换空格.md)
**还有链表找环,也用到双指针:**
* [0142.环形链表II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0142.环形链表II.md)
大家都可以去做一做,感受一下双指针法的内在逻辑!
# 本题C++代码
## 暴力解法
时间复杂度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;
}
};
```
> 更过算法干货文章持续更新可以微信搜索「代码随想录」第一时间围观关注后回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等就可以获得我多年整理的学习资料。
> 更多算法干货文章持续更新可以微信搜索「代码随想录」第一时间围观关注后回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等就可以获得我多年整理的学习资料。

View File

@ -0,0 +1,124 @@
## 题目地址
# 第242题.有效的字母异位词
# 思路
举一个有重复机场的例子:
<img src='../pics/332.重新安排行程.png' width=600> </img></div>
为什么要举这个例子呢,就是告诉大家,出发机场和到达机场也会重复的,**如果在解题的过程中没有对集合元素处理好,就会死循环。**
这道题目有几个难点:
1. 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢
2. 这是一个图不是一棵树,使用深搜/回溯 终止条件是什么呢?
3. 回溯的过程中,如何遍历一个城市所对应的所有城市。
首先这道题目是使用回溯法(也可以说深搜),那么按照我总结的回溯模板来。
```
backtracking() {
if (终止条件) {
存放结果;
}
for (枚举同一个位置的所有可能性,可以想成节点孩子的数量) {
递归,处理节点;
backtracking();
回溯,撤销处理结果
}
}
```
## 1. 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢
一个城市映射多个城市城市之间要靠字母序排列一个城市映射多个城市可以使用std::unordered_map如果让多个城市之间再有顺序的话就是用std::map 或者 或者std::multimap 或者 std::multiset。
如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学可以看这篇文章[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)。
这样存放映射关系可以定义为 `unordered_map<string, multiset<string>> targets` 或者 `unordered_map<string, map<string, int>> targets`
含义如下:
`unordered_map<string, multiset<string>> targets``unordered_map<出发城市, 到达城市的集合> targets`
`unordered_map<string, map<string, int>> targets``unordered_map<出发城市, map<到达城市, 航班次数>> targets`
这两个结构,我们选择了后者,因为如果使用`unordered_map<string, multiset<string>> targets` 遍历multiset的时候不能删除元素一旦删除元素迭代器就失效了。而本地在回溯的过程中就是要不断的增删 multiset里的元素所以 我们使用`unordered_map<string, map<string, int>> targets`
在遍历 `unordered_map<出发城市, map<到达城市, 航班次数>> targets`的过程中,可以使用航班次数这个字段的数字 --或者++,来标记到达城市是否使用过了,而不用对集合做删除元素或者增加元素的操作。
## 2. 这是一个图不是一棵树,使用深搜/回溯 终止条件是什么呢?
你看有多少个航班,那题目中的示例为例,输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] 这是有4个航班那么只要找出一种行程行程里的机场个数是5就可以了。
所以终止条件 我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1那么我们就找到了一个行程把所有航班串在一起了。
## 3. 回溯的过程中,如何遍历一个城市所对应的所有城市。
这里刚刚说过,在选择映射函数的时候,不能选择`unordered_map<string, multiset<string>> targets` 因为一旦有元素增删multiset的迭代器就会失效有一些题解使用了 例如list的迭代器使用splice来保证 迭代器不失效。
可以说既要找到一个对数据经行排序的容器,而且这个容易增删元素,迭代器还不能失效。
**再说一下为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除元素就会死循环。**
所以我选择了`unordered_map<string, map<string, int>> targets` 来基于映射条件。
遍历过程如下:
```
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
if (target.second > 0 ) {
result.push_back(target.first);
target.second--;
if (backtracking(ticketNum, index + 1, result)) return true;
result.pop_back();
target.second++;
}
}
```
可以看出 通过`unordered_map<string, map<string, int>> targets`里的int字段来判断 这个集合使用使用完了,这样避免了 增删元素。
此时完整代码如下:
# C++代码
```
class Solution {
private:
// unordered_map<出发城市, map<到达城市, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, int index, vector<string>& result) {
if (index == ticketNum + 1) {
return true;
}
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
if (target.second > 0 ) { // 使用int字段来记录到达城市是否使用过了
result.push_back(target.first);
target.second--;
if (backtracking(ticketNum, index + 1, result)) return true;
result.pop_back();
target.second++;
}
}
return false;
}
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
vector<string> result;
for (const vector<string>& vec : tickets) {
targets[vec[0]][vec[1]]++; // 记录映射关系
}
result.push_back("JFK");
backtracking(tickets.size(), 1, result);
return result;
}
};
```