This commit is contained in:
youngyangyang04
2020-09-18 16:51:32 +08:00
parent 634dcfb394
commit c761b59b50
24 changed files with 787 additions and 287 deletions

242
README.md
View File

@ -152,246 +152,7 @@
# 算法模板
## 二分查找法
```
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n; // 我们定义target在左闭右开的区间里[left, right)
while (left < right) { // 因为left == right的时候在[left, right)是无效的空间
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间因为是左闭右开的区间nums[middle]一定不是我们的目标值所以right = middle在[left, middle)中继续寻找目标值
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在 [middle+1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
return right;
}
};
```
## KMP
```
void kmp(int* next, const string& s){
next[0] = -1;
int j = -1;
for(int i = 1; i < s.size(); i++){
while (j >= 0 && s[i] != s[j + 1]) {
j = next[j];
}
if (s[i] == s[j + 1]) {
j++;
}
next[i] = j;
}
}
```
## 二叉树
二叉树的定义:
```
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
```
### 深度优先遍历(递归)
前序遍历(中左右)
```
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
```
中序遍历(左中右)
```
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方
traversal(cur->right, vec); // 右
}
```
中序遍历(中左右)
```
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
```
### 深度优先遍历(迭代法)
相关题解:[0094.二叉树的中序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0094.二叉树的中序遍历.md)
前序遍历(中左右)
```
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
st.push(node); // 中
st.push(NULL);
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val); // 节点处理逻辑
}
}
return result;
}
```
中序遍历(左中右)
```
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result; // 存放中序遍历的元素
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
if (node->right) st.push(node->right); // 右
st.push(node); // 中
st.push(NULL);
if (node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val); // 节点处理逻辑
}
}
return result;
}
```
后序遍历(左右中)
```
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
st.push(node); // 中
st.push(NULL);
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val); // 节点处理逻辑
}
}
return result;
}
```
### 广度优先遍历(队列)
相关题解:[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md)
```
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
for (int i = 0; i < size; i++) {// 这里一定要使用固定大小size不要使用que.size()
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val); // 节点处理的逻辑
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
```
可以直接解决如下题目:
* [0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md)
* [0199.二叉树的右视图](https://github.com/youngyangyang04/leetcode/blob/master/problems/0199.二叉树的右视图.md)
* [0637.二叉树的层平均值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0637.二叉树的层平均值.md)
* [0104.二叉树的最大深度 (迭代法)](https://github.com/youngyangyang04/leetcode/blob/master/problems/0104.二叉树的最大深度.md)
* [0111.二叉树的最小深度(迭代法)]((https://github.com/youngyangyang04/leetcode/blob/master/problems/0111.二叉树的最小深度.md))
* [0222.完全二叉树的节点个数(迭代法)](https://github.com/youngyangyang04/leetcode/blob/master/problems/0222.完全二叉树的节点个数.md)
### 二叉树深度
```
int getDepth(TreeNode* node) {
if (node == NULL) return 0;
return 1 + max(getDepth(node->left), getDepth(node->right));
}
```
### 二叉树节点数量
```
int countNodes(TreeNode* root) {
if (root == NULL) return 0;
return 1 + countNodes(root->left) + countNodes(root->right);
}
```
## 回溯算法
```
backtracking() {
if (终止条件) {
存放结果;
}
for (选择:选择列表(可以想成树中节点孩子的数量)) {
递归,处理节点;
backtracking();
回溯,撤销处理结果
}
}
```
持续补充ing
[各类基础算法模板](https://github.com/youngyangyang04/leetcode/blob/master/problems/算法模板.md)
# LeetCode 最强题解:
@ -469,6 +230,7 @@ backtracking() {
|[0617.合并二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0617.合并二叉树.md) |树 |简单|**递归** **迭代**|
|[0637.二叉树的层平均值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0637.二叉树的层平均值.md) |树 |简单|**广度优先搜索/队列**|
|[0654.最大二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0654.最大二叉树.md) |树 |中等|**递归**|
|[0685.冗余连接II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0685.冗余连接II.md) | 并查集/树/图 |困难|**并查集**|
|[0700.二叉搜索树中的搜索](https://github.com/youngyangyang04/leetcode/blob/master/problems/0700.二叉搜索树中的搜索.md) |树 |简单|**递归** **迭代**|
|[0701.二叉搜索树中的插入操作](https://github.com/youngyangyang04/leetcode/blob/master/problems/0701.二叉搜索树中的插入操作.md) |树 |简单|**递归** **迭代**|
|[0705.设计哈希集合](https://github.com/youngyangyang04/leetcode/blob/master/problems/0705.设计哈希集合.md) |哈希表 |简单|**模拟**|

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 127 KiB

BIN
pics/47.全排列II1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

BIN
pics/47.全排列II2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
pics/47.全排列II3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
pics/78.子集.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
pics/90.子集II.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

View File

@ -3,9 +3,31 @@ https://leetcode-cn.com/problems/permutations-ii/
## 思路
i > 0 && nums[i] == nums[i-1] && used[i-1] == false
这道题目和46.全排列的区别在与**给定一个可包含重复数字的序列**,要返回**所有不重复的全排列**。
是最高效的,可以用 1 1 1 1 1 跑一个样例试试
里就涉及到去重了。
要注意**全排列是要取树的子节点的,如果是子集问题,就取树上的所有节点。**
很多同学在去重上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。
这个去重为什么很难理解呢,**所谓去重,其实就是使用过的元素不能重复选取。** 这么一说好像很简单!
但是什么又是“使用过”,我们把排列问题抽象为树形结构之后,**“使用过”在这个树形结构上是有两个维度的**,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。
**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。**
那么排列问题,既可以在 同一树层上的“使用过”来去重,也可以在同一树枝上的“使用过”来去重!
理解这一本质,很多疑点就迎刃而解了。
首先把示例中的 [1,1,2]抽象为一棵树然后在同一树层上对nums[i-1]使用过的话,进行去重如图:
<img src='../pics/47.全排列II1.png' width=600> </img></div>
图中我们对同一树层前一位也就是nums[i-1])如果使用过,那么就进行去重。
代码如下:
## C++代码
@ -21,7 +43,11 @@ private:
}
for (int i = 0; i < nums.size(); i++) {
if (i > 0 && nums[i] == nums[i-1] && used[i-1] == false) {
// 这里理解used[i - 1]非常重要
// used[i - 1] == true说明同一树支nums[i - 1]使用过
// used[i - 1] == false说明同一树层nums[i - 1]使用过
// 如果同一树层nums[i - 1]使用过则直接跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
if (used[i] == false) {
@ -45,3 +71,39 @@ public:
}
};
```
## 拓展
大家发现,去重最为关键的代码为:
```
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
```
可是如果把 `used[i - 1] == true` 也是正确的,去重代码如下:
```
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
continue;
}
```
这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用`used[i - 1] == false`,如果要对树枝前一位去重用用`used[i - 1] == true`
**对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!**
这么说是不是有点抽象?
来来来,我就用输入: [1,1,1] 来举一个例子。
树层上去重(used[i - 1] == false),的树形结构如下:
<img src='../pics/47.全排列II2.png' width=600> </img></div>
树枝上去重used[i - 1] == true的树型结构如下
<img src='../pics/47.全排列II3.png' width=600> </img></div>
大家应该很清晰的看到,树层上去重非常彻底,效率很高,树枝上去重虽然最后可能得到答案,但是多做了很多无用搜索。

View File

@ -23,26 +23,90 @@
# 思路
求子集问题和 求组合组合和分割问题又不一样了, 如何把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是找树的叶子节点,而子集问题是找树的所有节点!
取子集也是,其实也是一种组合位置,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。 那么既然是无序写回溯算法的时候for就要从startIndex开始而不是从0开始
那有同学问题什么时候for可以从0开始求排列问题的时候就要从0开始因为集合是有序的{1, 2} 和{2, 1}是两个集合。
以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下:
<img src='../pics/78.子集.png' width=600> </img></div>
从图中,可以看出,遍历这个树的时候,把所有节点都记录下来,就是要求的子集。
来看一下我总结的回溯模板来:
```
backtracking() {
if (终止条件) {
存放结果;
}
for (选择:选择列表(可以想成树中节点孩子的数量)) {
递归,处理节点;
backtracking();
回溯,撤销处理结果
}
}
```
首先是终止条件终止条件就是startIndex已经大于数组的长度了就是终止了代码如下:
```
if (startIndex >= nums.size()) {
return;
}
```
但是,要明确的是,**求取子集问题,其实没有必要加终止条件,因为子集就是要遍历整个一棵树,不需要任何剪枝!**
大家一会看到下面整体代码的时候就知道了。
然后就是看如何写for循环**因为求子集也是无序的所以for循环要从startIndex开始**
代码如下:
```
for (int i = startIndex; i < nums.size(); i++) {
```
接下来就是递归与回溯,定一个`vector<int> path`用来收集子集的元素在回溯的时候还要弹出backtracking每次调用自己的时候记着要从i+1 开始,代码如下:
```
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
```
重点代码分析完之后,整体代码如下:
可以发现我在backtracking里并没有写终止条件因为本来我们就要遍历整颗树。
有的同学可能担心会不会无限递归? 并不会因为每次递归的下一层就是从i+1开始的。
# C++代码
```
class Solution {
private:
void backtracking(vector<int>& nums, vector<vector<int>>& result, vector<int>& vec, int startIndex) {
result.push_back(vec);
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
result.push_back(path);
for (int i = startIndex; i < nums.size(); i++) {
vec.push_back(nums[i]);
backtracking(nums, result, vec, i + 1);
vec.pop_back();
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
vector<int> vec;
backtracking(nums, result, vec, 0);
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};

View File

@ -1,10 +1,44 @@
# 题目地址
https://leetcode-cn.com/problems/subsets-ii/
# 第90题. 子集II
给定一个可能包含重复元素的整数数组 nums返回该数组所有可能的子集幂集
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
# 思路
这道题目和[0078.子集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0078.子集.md)区别就是集合里有重复元素了,而且求取的子集要去重。
很多同学在去重上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。
这个去重为什么很难理解呢,**所谓去重,其实就是使用过的元素不能重复选取。** 这么一说好像很简单!
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。**
所以要明确我们要去重的是同一树层上的“使用过”。
用示例中的[1, 2, 2] 来举例,如图所示:
<img src='../pics/90.子集II.png' width=600> </img></div>
从图中可以看出同一树层上对重复取2 就要过滤掉同一树枝上就可以重复取2因为同一树枝上元素的集合才是唯一子集
代码如下:(已经详细注释)
# C++代码
@ -14,7 +48,10 @@ private:
void backtracking(vector<int>& nums, vector<vector<int>>& result, vector<int>& vec, int startIndex, vector<bool>& used) {
result.push_back(vec);
for (int i = startIndex; i < nums.size(); i++) {
if (i > 0 && nums[i] == nums[i - 1] && used[i-1] == false) { // 果然去重都是一个逻辑啊
// used[i - 1] == true说明同一树支candidates[i - 1]使用过
// used[i - 1] == false说明同一树层candidates[i - 1]使用过
// 而我们要对同一树层使用过的元素进行跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
vec.push_back(nums[i]);

View File

@ -222,12 +222,12 @@ public:
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node->right) st.push(node->right); // 添加右节点
if (node->right) st.push(node->right); // 添加右节点
st.push(node); // 添加中节点
st.push(node); // 添加中节点
st.push(NULL); // 中节点访问过,但是还没有处理,需要做一下标记。
if (node->left) st.push(node->left); // 添加左节点
if (node->left) st.push(node->left); // 添加左节点
} else {
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
@ -240,11 +240,11 @@ public:
};
```
看代码有点抽象我们来看一下动画:
看代码有点抽象我们来看一下动画(中序遍历)
<video src='../video/中序遍历迭代(统一写法).mp4' controls='controls' width='640' height='320' autoplay='autoplay'> Your browser does not support the video tag.</video></div>
前序遍历代码如下:
前序遍历代码如下: (**注意此时我们仅仅和中序遍历相对改变了两行代码的顺序**)
```
class Solution {
@ -273,7 +273,7 @@ public:
};
```
后续遍历代码如下:
后续遍历代码如下: (**注意此时我们仅仅和中序遍历相对改变了两行代码的顺序**)
```
class Solution {

View File

@ -1,32 +1,117 @@
## 题目地址
https://leetcode-cn.com/problems/sliding-window-maximum/
## 思路
> 要用啥数据结构呢?
# 239. 滑动窗口最大值
给定一个数组 nums有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
 
<img src='../pics/239.滑动窗口最大值.png' width=600> </img></div>
提示:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
1 <= k <= nums.length
# 思路
这是使用单调队列的经典题目。
难点是如何求一个区间里的最大值呢? (这好像是废话),暴力一下不就得了。
暴力方法遍历一遍的过程中每次从窗口中在找到最大的数值这样很明显是O(n * k)的算法。
有的同学可能会想用一个大顶堆也就是优先级队列来存放这个窗口里的k个数字这样就可以知道最大的最大值是多少了 **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。**
有的同学可能会想用一个大顶堆优先级队列来存放这个窗口里的k个数字这样就可以知道最大的最大值是多少了 **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。**
**使用单调队列,即单调递减或单调递增的队列。它不是一个独立的数据结构,需要使用其他数据结构来实现单调队列**,例如: dequedeque是双向队列可以选择 从头部或者尾部pop同样也可以从头部或者尾部push。
此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。
使用deque实现的单调队列如下代码详细注释
这个队列应该长这个样子:
```
class MyQueue {
public:
void pop(int value) {
}
void push(int value) {
}
int front() {
return que.front();
}
};
```
每次窗口移动的时候调用que.pop(滑动窗口中移除元素的数值)que.push(滑动窗口添加元素的数值)然后que.front()就返回我们要的最大值。
这么个队列香不香,要是有现成的这种数据结构是不是更香了!
**可惜了,没有! 我们需要自己实现这么个队列。**
然后在分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。
但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。
那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。
大家此时应该陷入深思.....
**其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。**
那么这个维护元素单调递减的队列就叫做**单调队列即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列**
**不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。**
来看一下单调队列如何维护队列里的元素。
动画如下:
<img src='../video/239.滑动窗口最大值.gif' width=600> </img></div>
对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。
此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口经行滑动呢?
设计单调队列的时候pop和push操作要保持如下规则
1. pop(value)如果窗口移除的元素value等于单调队列的出口元素那么队列弹出元素否则不用任何操作
2. push(value)如果push的元素value大于入口元素的数值那么就将队列出口的元素弹出直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则每次窗口移动的时候只要问que.front()就可以返回当前窗口的最大值。
为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3动画如下
<video src='../video/239.滑动窗口最大值.mp4' controls='controls' width='640' height='320' autoplay='autoplay'> Your browser does not support the video tag.</video></div>
那么我们用什么数据结构来实现这个单调队列呢?
使用deque最为合适在文章[栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ)中我们就提到了常用的queue在没有指定容器的情况下deque就是默认底层容器。
基于刚刚说过的单调队列pop和push的规则代码不难实现如下
```
class MyQueue { //单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列前端的数值,如果相等在pop数据当然也要判断队列当前是否为空
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出
// 同时pop之前判断队列当前是否为空。
void pop(int value) {
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于后端的数值那么就将队列后端的数值弹出直到push的数值小于等于 队列后端的数值为止。
// 然后再将数值push到队列后端这样就保持了队列里的数值是单调从大到小的了。
// 如果push的数值大于入口元素的数值那么就将队列后端的数值弹出直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
@ -41,27 +126,26 @@ public:
};
```
动画解释如下:
<video src='../video/0239.滑动窗口最大值.mp4' controls='controls' width='640' height='320' autoplay='autoplay'> Your browser does not support the video tag.</video></div>
这样我们就用deque实现了一个单调队列接下来解决滑动窗口最大值的问题就很简单了直接看代码吧。
这样我们就用deque实现了一个单调队列接下来解决滑动窗口最大值的问题就很简单了。
详情看代码吧,已经简洁。
## C++代码
# C++代码
```
class Solution {
public:
private:
class MyQueue { //单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
void pop(int value) {
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于入口元素的数值那么就将队列后端的数值弹出直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
@ -69,10 +153,12 @@ public:
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
@ -81,21 +167,30 @@ public:
}
result.push_back(que.front()); // result 记录前k的元素的最大值
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 模拟滑动窗口的移动
que.push(nums[i]); // 模拟滑动窗口的移动
que.pop(nums[i - k]); // 滑动窗口移除最前面元素
que.push(nums[i]); // 滑动窗口前加入最后面的元素
result.push_back(que.front()); // 记录对应的最大值
}
return result;
}
};
```
来看一下时间复杂度,时间复杂度: O(n)
有的同学可能想了,在队里中 push元素的过程中还有pop操作呢感觉不是纯粹了O(n)。
其实大家可以自己观察一下单调队列的实现nums 中的每个元素最多也就被 push_back 和 pop_back 一次,没有任何多余操作,所以整体的复杂度是 O(n)。
在来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。
有的同学可能想了,在队列中 push元素的过程中还有pop操作呢感觉不是纯粹的O(n)。
其实大家可以自己观察一下单调队列的实现nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。
空间复杂度因为我们定义一个辅助队列所以是O(k)。
# 扩展
大家貌似对单调队列 都有一些疑惑首先要明确的是题解中单调队列里的pop和push接口仅适用于本题哈。单调队列不是一成不变的而是不同场景不同写法总之要保证队列里单调递减或递增的原则所以叫做单调队列。 不要以为本地中的单调队列实现就是固定的写法哈。
大家貌似对deque也有一些疑惑C++中deque是stack和queue默认的底层实现容器这个我们之前已经讲过啦deque是可以两边扩展的而且deque里元素并不是严格的连续分布的。
> 更过算法干货文章持续更新可以微信搜索「代码随想录」第一时间围观关注后回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等就可以获得我多年整理的学习资料。

View File

@ -2,7 +2,27 @@
https://leetcode-cn.com/problems/top-k-frequent-elements/
## 思路
> 前K个大数问题老生常谈不得不谈
# 347.前 K 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
你可以按任意顺序返回答案。
# 思路
这道题目主要涉及到如下三块内容:
1. 要统计元素出现频率
@ -11,11 +31,42 @@ https://leetcode-cn.com/problems/top-k-frequent-elements/
首先统计元素出现的频率这一类的问题可以使用map来进行统计。
然后是对频率进行排序,这里我们可以使用一种 容器适配器就是优先级队列。 为什么不用快排呢, 使用快排我们要向map转换为vector的结构然后对整个数组进行排序 而这种场景下我们其实只需要维护k个有序的序列就可以了所以使用优先级队列是最优的。 **需要注意的是我们要定一个小顶堆** 因为要统计最大前k个元素只有小顶堆每次将最小的元素弹出最后小顶堆里积累的才是前k个最大元素。
然后是对频率进行排序,这里我们可以使用一种 容器适配器就是**优先级队列**。
最后我们从优先级队列里找出前k个元素就可以了。
什么是优先级队列呢?
## C++代码
其实**就是一个披着队列外衣的堆**,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?
缺省情况下priority_queue利用max-heap大顶堆完成对元素的排序这个大顶堆是以vector为表现形式的complete binary tree完全二叉树
什么是堆呢?
**堆是一颗完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。** 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
所以大家经常说的大顶堆堆头是最大元素小顶堆堆头是最小元素如果懒得自己实现的话就直接用priority_queue优先级队列就可以了底层实现都是一样的从小到大排就是小顶堆从大到小排就是大顶堆。
本题我们就要使用优先级队列来对部分频率进行排序。
为什么不用快排呢, 使用快排要将map转换为vector的结构然后对整个数组进行排序 而这种场景下我们其实只需要维护k个有序的序列就可以了所以使用优先级队列是最优的。
此时要思考一下,是使用小顶堆呢,还是大顶堆?
有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。
那么问题来了定义一个大小为k的大顶堆在每次移动更新大顶堆的时候每次弹出都把最大的元素弹出去了那么怎么保留下来前K个高频元素呢。
**所以我们要用小顶堆因为要统计最大前k个元素只有小顶堆每次将最小的元素弹出最后小顶堆里积累的才是前k个最大元素。**
寻找前k个最大元素流程如图所示图中的频率只有三个所以正好构成一个大小为3的小顶堆如果频率更多一些则用这个小顶堆进行扫描
<img src='../pics/347.前K个高频元素.png' width=600> </img></div>
我们来看一下代码:
# C++代码
```
// 时间复杂度O(nlogk)
@ -39,14 +90,16 @@ public:
// 对频率排序
// 定义一个小顶堆大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆扫面所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
pri_que.push(*it);
if (pri_que.size() > k) {
if (pri_que.size() > k) { // 如果堆的大小大于了K则队列弹出保证堆的大小一直为k
pri_que.pop();
}
}
// 找出前K个高频元素因为小顶堆先弹出的是最小的所以倒叙来数值数组
// 找出前K个高频元素因为小顶堆先弹出的是最小的所以倒叙来输出到数组
vector<int> result(k);
for (int i = k - 1; i >= 0; i--) {
result[i] = pri_que.top().first;

View File

@ -0,0 +1,175 @@
## 题目地址
https://leetcode-cn.com/problems/redundant-connection-ii/
## 思路
先重点读懂题目中的这句**该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间这条附加的边不属于树中已存在的边。**
**这说明题目中的图原本是是一棵树,只不过在不增加节点的情况下多加了一条边!**
还有**若有多个答案,返回最后出现在给定二维数组的答案。**这说明在两天边都可以删除的情况下,要删顺序靠后的!
那么有如下三种情况前两种情况是出现入度为2的点如图
<img src='../pics/685.冗余连接II1.png' width=600> </img></div>
且只有一个节点入度为2为什么不看出度呢出度没有意义一颗树中随便一个父节点就有多个出度。
第三种情况是没有入度为2的点那么图中一定出现了有向环**注意这里强调是有向环!**
如图:
<img src='../pics/685.冗余连接II2.png' width=600> </img></div>
首先先计算节点的入度,代码如下:
```
int inDegree[N] = {0}; // 记录节点入度
n = edges.size(); // 边的数量
for (int i = 0; i < n; i++) {
inDegree[edges[i][1]]++; // 统计入度
}
```
前两种入度为2的情况一定是删除指向入度为2的节点的两条边其中的一条如果删了一条判断这个图是一个树那么这条边就是答案同时注意要从后向前遍历因为如果两天边删哪一条都可以成为树就删最后那一条。
代码如下:
```
vector<int> vec; // 记录入度为2的边如果有的话就两条边
// 找入度为2的节点所对应的边注意要倒叙因为优先返回最后出现在二维数组中的答案
for (int i = n - 1; i >= 0; i--) {
if (inDegree[edges[i][1]] == 2) {
vec.push_back(i);
}
}
// 处理图中情况1 和 情况2
// 如果有入度为2的节点那么一定是两条边里删一个看删哪个可以构成树
if (vec.size() > 0) {
if (isTreeAfterRemoveEdge(edges, vec[0])) {
return edges[vec[0]];
} else {
return edges[vec[1]];
}
}
```
在来看情况三明确没有入度为2的情况那么一定有有向环找到构成环的边就是要删除的边。
可以定义一个函数,代码如下:
```
// 在有向图里找到删除的那条边,使其变成树,返回值就是要删除的边
vector<int> getRemoveEdge(const vector<vector<int>>& edges)
```
此时 大家应该知道了,我们要实现两个最为关键的函数:
* `isTreeAfterRemoveEdge()` 判断删一个边之后是不是树了
* `getRemoveEdge` 确定图中一定有了有向环,那么要找到需要删除的那条边
此时应该是用到**并查集**了,并查集为什么可以判断 一个图是不是树呢?
**因为如果两个点所在的边在添加图之前如果就可以在并查集里找到了相同的根,那么这条边添加上之后 这个图一定不是树了**
这里对并查集就不展开过多的讲解了,翻到了自己九年前写过了一篇并查集的文章[并查集学习](https://blog.csdn.net/youngyangyang04/article/details/6447435),哈哈,那时候还太年轻,写不咋地,有空我会重写一篇!
本题代码如下:(详细注释了)
## C++代码
```
class Solution {
private:
static const int N = 1010; // 如题二维数组大小的在3到1000范围内
int father[N];
int n; // 边的数量
// 并查集初始化
void init() {
for (int i = 1; i <= n; ++i) {
father[i] = i;
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return ;
father[v] = u;
}
// 判断 u 和 v是否找到同一个根
bool same(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 在有向图里找到删除的那条边,使其变成树
vector<int> getRemoveEdge(const vector<vector<int>>& edges) {
init(); // 初始化并查集
for (int i = 0; i < n; i++) { // 遍历所有的边
if (same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边
return edges[i];
}
join(edges[i][0], edges[i][1]);
}
return {};
}
// 删一条边之后判断是不是树
bool isTreeAfterRemoveEdge(const vector<vector<int>>& edges, int deleteEdge) {
init(); // 初始化并查集
for (int i = 0; i < n; i++) {
if (i == deleteEdge) continue;
if (same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树
return false;
}
join(edges[i][0], edges[i][1]);
}
return true;
}
public:
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int inDegree[N] = {0}; // 记录节点入度
n = edges.size(); // 边的数量
for (int i = 0; i < n; i++) {
inDegree[edges[i][1]]++; // 统计入度
}
vector<int> vec; // 记录入度为2的边如果有的话就两条边
// 找入度为2的节点所对应的边注意要倒叙因为优先返回最后出现在二维数组中的答案
for (int i = n - 1; i >= 0; i--) {
if (inDegree[edges[i][1]] == 2) {
vec.push_back(i);
}
}
// 处理图中情况1 和 情况2
// 如果有入度为2的节点那么一定是两条边里删一个看删哪个可以构成树
if (vec.size() > 0) {
if (isTreeAfterRemoveEdge(edges, vec[0])) {
return edges[vec[0]];
} else {
return edges[vec[1]];
}
}
// 处理图中情况3
// 明确没有入度为2的情况那么一定有有向环找到构成环的边返回就可以了
return getRemoveEdge(edges);
}
};
```

View File

@ -3,4 +3,15 @@
* 切割问题
* 子集问题
* 排列问题
* 皇后问题
* 棋盘问题
* 皇后问题
* 解数独
# 单层递归
# 双层递归
# 组合 子集问题used[i-1] = false 来去重复, 啥问题 used[i-1] = true也是可以的来着

241
problems/算法模板.md Normal file
View File

@ -0,0 +1,241 @@
## 二分查找法
```
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n; // 我们定义target在左闭右开的区间里[left, right)
while (left < right) { // 因为left == right的时候在[left, right)是无效的空间
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间因为是左闭右开的区间nums[middle]一定不是我们的目标值所以right = middle在[left, middle)中继续寻找目标值
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在 [middle+1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
return right;
}
};
```
## KMP
```
void kmp(int* next, const string& s){
next[0] = -1;
int j = -1;
for(int i = 1; i < s.size(); i++){
while (j >= 0 && s[i] != s[j + 1]) {
j = next[j];
}
if (s[i] == s[j + 1]) {
j++;
}
next[i] = j;
}
}
```
## 二叉树
二叉树的定义:
```
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
```
### 深度优先遍历(递归)
前序遍历(中左右)
```
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
```
中序遍历(左中右)
```
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方
traversal(cur->right, vec); // 右
}
```
中序遍历(中左右)
```
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
```
### 深度优先遍历(迭代法)
相关题解:[0094.二叉树的中序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0094.二叉树的中序遍历.md)
前序遍历(中左右)
```
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
st.push(node); // 中
st.push(NULL);
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val); // 节点处理逻辑
}
}
return result;
}
```
中序遍历(左中右)
```
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result; // 存放中序遍历的元素
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
if (node->right) st.push(node->right); // 右
st.push(node); // 中
st.push(NULL);
if (node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val); // 节点处理逻辑
}
}
return result;
}
```
后序遍历(左右中)
```
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
st.push(node); // 中
st.push(NULL);
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val); // 节点处理逻辑
}
}
return result;
}
```
### 广度优先遍历(队列)
相关题解:[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md)
```
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
for (int i = 0; i < size; i++) {// 这里一定要使用固定大小size不要使用que.size()
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val); // 节点处理的逻辑
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
```
可以直接解决如下题目:
* [0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md)
* [0199.二叉树的右视图](https://github.com/youngyangyang04/leetcode/blob/master/problems/0199.二叉树的右视图.md)
* [0637.二叉树的层平均值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0637.二叉树的层平均值.md)
* [0104.二叉树的最大深度 (迭代法)](https://github.com/youngyangyang04/leetcode/blob/master/problems/0104.二叉树的最大深度.md)
* [0111.二叉树的最小深度(迭代法)]((https://github.com/youngyangyang04/leetcode/blob/master/problems/0111.二叉树的最小深度.md))
* [0222.完全二叉树的节点个数(迭代法)](https://github.com/youngyangyang04/leetcode/blob/master/problems/0222.完全二叉树的节点个数.md)
### 二叉树深度
```
int getDepth(TreeNode* node) {
if (node == NULL) return 0;
return 1 + max(getDepth(node->left), getDepth(node->right));
}
```
### 二叉树节点数量
```
int countNodes(TreeNode* root) {
if (root == NULL) return 0;
return 1 + countNodes(root->left) + countNodes(root->right);
}
```
## 回溯算法
```
backtracking() {
if (终止条件) {
存放结果;
}
for (选择:选择列表(可以想成树中节点孩子的数量)) {
递归,处理节点;
backtracking();
回溯,撤销处理结果
}
}
```
持续补充ing

BIN
video/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

Binary file not shown.