diff --git a/README.md b/README.md index 66ed1706..4b311a00 100644 --- a/README.md +++ b/README.md @@ -152,246 +152,7 @@ # 算法模板 -## 二分查找法 - -``` -class Solution { -public: - int searchInsert(vector& 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& vec) { - if (cur == NULL) return; - vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 - traversal(cur->left, vec); // 左 - traversal(cur->right, vec); // 右 -} -``` -中序遍历(左中右) -``` -void traversal(TreeNode* cur, vector& vec) { - if (cur == NULL) return; - traversal(cur->left, vec); // 左 - vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 - traversal(cur->right, vec); // 右 -} -``` -中序遍历(中左右) -``` -void traversal(TreeNode* cur, vector& 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 preorderTraversal(TreeNode* root) { - vector result; - stack 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 inorderTraversal(TreeNode* root) { - vector result; // 存放中序遍历的元素 - stack 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 postorderTraversal(TreeNode* root) { - vector result; - stack 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> levelOrder(TreeNode* root) { - queue que; - if (root != NULL) que.push(root); - vector> result; - while (!que.empty()) { - int size = que.size(); - vector 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) |哈希表 |简单|**模拟**| diff --git a/pics/239.滑动窗口最大值.png b/pics/239.滑动窗口最大值.png new file mode 100644 index 00000000..49734e5d Binary files /dev/null and b/pics/239.滑动窗口最大值.png differ diff --git a/pics/347.前K个高频元素.png b/pics/347.前K个高频元素.png index 0d665000..54b1417d 100644 Binary files a/pics/347.前K个高频元素.png and b/pics/347.前K个高频元素.png differ diff --git a/pics/47.全排列II1.png b/pics/47.全排列II1.png new file mode 100644 index 00000000..e606d9fe Binary files /dev/null and b/pics/47.全排列II1.png differ diff --git a/pics/47.全排列II2.png b/pics/47.全排列II2.png new file mode 100644 index 00000000..9c1e98f2 Binary files /dev/null and b/pics/47.全排列II2.png differ diff --git a/pics/47.全排列II3.png b/pics/47.全排列II3.png new file mode 100644 index 00000000..70236cb3 Binary files /dev/null and b/pics/47.全排列II3.png differ diff --git a/pics/685.冗余连接II1.png b/pics/685.冗余连接II1.png new file mode 100644 index 00000000..ab833087 Binary files /dev/null and b/pics/685.冗余连接II1.png differ diff --git a/pics/685.冗余连接II2.png b/pics/685.冗余连接II2.png new file mode 100644 index 00000000..6dbb2ac7 Binary files /dev/null and b/pics/685.冗余连接II2.png differ diff --git a/pics/78.子集.png b/pics/78.子集.png new file mode 100644 index 00000000..1700030f Binary files /dev/null and b/pics/78.子集.png differ diff --git a/pics/90.子集II.png b/pics/90.子集II.png new file mode 100644 index 00000000..ed3e195d Binary files /dev/null and b/pics/90.子集II.png differ diff --git a/problems/0047.全排列II.md b/problems/0047.全排列II.md index 7e968eb4..289c86b6 100644 --- a/problems/0047.全排列II.md +++ b/problems/0047.全排列II.md @@ -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]使用过的话,进行去重如图: + + + +图中我们对同一树层,前一位(也就是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),的树形结构如下: + + + +树枝上去重(used[i - 1] == true)的树型结构如下: + + + +大家应该很清晰的看到,树层上去重非常彻底,效率很高,树枝上去重虽然最后可能得到答案,但是多做了很多无用搜索。 + diff --git a/problems/0078.子集.md b/problems/0078.子集.md index ee84f70d..4987ed66 100644 --- a/problems/0078.子集.md +++ b/problems/0078.子集.md @@ -23,26 +23,90 @@ # 思路 +求子集问题和 求组合组合和分割问题又不一样了, 如何把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是找树的叶子节点,而子集问题是找树的所有节点! + +取子集也是,其实也是一种组合位置,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。 那么既然是无序,写回溯算法的时候,for就要从startIndex开始,而不是从0开始! + +那有同学问题,什么时候,for可以从0开始,求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合。 + +以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下: + + + +从图中,可以看出,遍历这个树的时候,把所有节点都记录下来,就是要求的子集。 + +来看一下我总结的回溯模板来: + +``` +backtracking() { + if (终止条件) { + 存放结果; + } + + for (选择:选择列表(可以想成树中节点孩子的数量)) { + 递归,处理节点; + backtracking(); + 回溯,撤销处理结果 + } +} +``` + +首先是终止条件,终止条件,就是startIndex已经大于数组的长度了,就是终止了,代码如下: + +``` +if (startIndex >= nums.size()) { + return; +} +``` + +但是,要明确的是,**求取子集问题,其实没有必要加终止条件,因为子集就是要遍历整个一棵树,不需要任何剪枝!** + +大家一会看到下面整体代码的时候就知道了。 + +然后就是看如何写for循环,**因为求子集也是无序的,所以for循环要从startIndex开始!** + +代码如下: + +``` +for (int i = startIndex; i < nums.size(); i++) { +``` + +接下来就是递归与回溯,定一个`vector 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& nums, vector>& result, vector& vec, int startIndex) { - result.push_back(vec); + vector> result; + vector path; + void backtracking(vector& 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> subsets(vector& nums) { - vector> result; - vector vec; - backtracking(nums, result, vec, 0); + result.clear(); + path.clear(); + backtracking(nums, 0); return result; } }; diff --git a/problems/0090.子集II.md b/problems/0090.子集II.md index 6e516536..ee7a7277 100644 --- a/problems/0090.子集II.md +++ b/problems/0090.子集II.md @@ -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] 来举例,如图所示: + + + +从图中可以看出,同一树层上对重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集! + +代码如下:(已经详细注释) # C++代码 @@ -14,7 +48,10 @@ private: void backtracking(vector& nums, vector>& result, vector& vec, int startIndex, vector& 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]); diff --git a/problems/0144.二叉树的前序遍历.md b/problems/0144.二叉树的前序遍历.md index d4f6c3e8..c020cc69 100644 --- a/problems/0144.二叉树的前序遍历.md +++ b/problems/0144.二叉树的前序遍历.md @@ -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: }; ``` -看代码有点抽象我们来看一下动画: +看代码有点抽象我们来看一下动画(中序遍历): -前序遍历代码如下: +前序遍历代码如下: (**注意此时我们仅仅和中序遍历相对改变了两行代码的顺序**) ``` class Solution { @@ -273,7 +273,7 @@ public: }; ``` -后续遍历代码如下: +后续遍历代码如下: (**注意此时我们仅仅和中序遍历相对改变了两行代码的顺序**) ``` class Solution { diff --git a/problems/0239.滑动窗口最大值.md b/problems/0239.滑动窗口最大值.md index 513fcdc2..603de230 100644 --- a/problems/0239.滑动窗口最大值.md +++ b/problems/0239.滑动窗口最大值.md @@ -1,32 +1,117 @@ ## 题目地址 https://leetcode-cn.com/problems/sliding-window-maximum/ -## 思路 +> 要用啥数据结构呢? + +# 239. 滑动窗口最大值 + +给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 + +返回滑动窗口中的最大值。 + +进阶: + +你能在线性时间复杂度内解决此题吗? + +  + + +提示: + +1 <= nums.length <= 10^5 +-10^4 <= nums[i] <= 10^4 +1 <= k <= nums.length + + + +# 思路 这是使用单调队列的经典题目。 +难点是如何求一个区间里的最大值呢? (这好像是废话),暴力一下不就得了。 + 暴力方法,遍历一遍的过程中每次从窗口中在找到最大的数值,这样很明显是O(n * k)的算法。 -有的同学可能会想用一个大顶堆也就是优先级队列来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了, **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。** +有的同学可能会想用一个大顶堆(优先级队列)来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了, **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。** -**使用单调队列,即单调递减或单调递增的队列。它不是一个独立的数据结构,需要使用其他数据结构来实现单调队列**,例如: deque,deque是双向队列,可以选择 从头部或者尾部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++中没有直接支持单调队列,需要我们自己来一个单调队列** + +**不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。** + +来看一下单调队列如何维护队列里的元素。 + +动画如下: + + + +对于窗口里的元素{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,动画如下: + + + +那么我们用什么数据结构来实现这个单调队列呢? + +使用deque最为合适,在文章[栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ)中,我们就提到了常用的queue在没有指定容器的情况下,deque就是默认底层容器。 + +基于刚刚说过的单调队列pop和push的规则,代码不难实现,如下: ``` class MyQueue { //单调队列(从大到小) public: deque 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: }; ``` -动画解释如下: - +这样我们就用deque实现了一个单调队列,接下来解决滑动窗口最大值的问题就很简单了,直接看代码吧。 -这样我们就用deque实现了一个单调队列,接下来解决滑动窗口最大值的问题就很简单了。 - -详情看代码吧,已经简洁。 - -## C++代码 +# C++代码 ``` class Solution { -public: +private: class MyQueue { //单调队列(从大到小) public: deque 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 maxSlidingWindow(vector& nums, int k) { MyQueue que; vector 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」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0347.前K个高频元素.md b/problems/0347.前K个高频元素.md index c572f6ca..394b08d2 100644 --- a/problems/0347.前K个高频元素.md +++ b/problems/0347.前K个高频元素.md @@ -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的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描) + + + + +我们来看一下代码: + +# C++代码 ``` // 时间复杂度:O(nlogk) @@ -39,14 +90,16 @@ public: // 对频率排序 // 定义一个小顶堆,大小为k priority_queue, vector>, mycomparison> pri_que; + + // 用固定大小为k的小顶堆,扫面所有频率的数值 for (unordered_map::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 result(k); for (int i = k - 1; i >= 0; i--) { result[i] = pri_que.top().first; diff --git a/problems/0685.冗余连接II.md b/problems/0685.冗余连接II.md new file mode 100644 index 00000000..d8b5f5ec --- /dev/null +++ b/problems/0685.冗余连接II.md @@ -0,0 +1,175 @@ +## 题目地址 +https://leetcode-cn.com/problems/redundant-connection-ii/ + +## 思路 + +先重点读懂题目中的这句**该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。** + +**这说明题目中的图原本是是一棵树,只不过在不增加节点的情况下多加了一条边!** + +还有**若有多个答案,返回最后出现在给定二维数组的答案。**这说明在两天边都可以删除的情况下,要删顺序靠后的! + + +那么有如下三种情况,前两种情况是出现入度为2的点,如图: + + + +且只有一个节点入度为2,为什么不看出度呢,出度没有意义,一颗树中随便一个父节点就有多个出度。 + + +第三种情况是没有入度为2的点,那么图中一定出现了有向环(**注意这里强调是有向环!**) + +如图: + + + + +首先先计算节点的入度,代码如下: + +``` + int inDegree[N] = {0}; // 记录节点入度 + n = edges.size(); // 边的数量 + for (int i = 0; i < n; i++) { + inDegree[edges[i][1]]++; // 统计入度 + } +``` + +前两种入度为2的情况,一定是删除指向入度为2的节点的两条边其中的一条,如果删了一条,判断这个图是一个树,那么这条边就是答案,同时注意要从后向前遍历,因为如果两天边删哪一条都可以成为树,就删最后那一条。 + +代码如下: + +``` + vector 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 getRemoveEdge(const vector>& 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 getRemoveEdge(const vector>& 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>& 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 findRedundantDirectedConnection(vector>& edges) { + int inDegree[N] = {0}; // 记录节点入度 + n = edges.size(); // 边的数量 + for (int i = 0; i < n; i++) { + inDegree[edges[i][1]]++; // 统计入度 + } + vector 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); + + } +}; +``` + + + + + + + diff --git a/problems/回溯总结.md b/problems/回溯总结.md index ebece54e..d29c9229 100644 --- a/problems/回溯总结.md +++ b/problems/回溯总结.md @@ -3,4 +3,15 @@ * 切割问题 * 子集问题 * 排列问题 -* 皇后问题 +* 棋盘问题 + * 皇后问题 + * 解数独 + + + +# 单层递归 + +# 双层递归 + + +# 组合 子集问题,used[i-1] = false 来去重复, 啥问题 used[i-1] = true也是可以的来着 diff --git a/problems/子集组合分割排列棋盘问题总结.md b/problems/子集组合分割排列棋盘问题总结.md new file mode 100644 index 00000000..e69de29b diff --git a/problems/算法模板.md b/problems/算法模板.md new file mode 100644 index 00000000..2656e62f --- /dev/null +++ b/problems/算法模板.md @@ -0,0 +1,241 @@ + +## 二分查找法 + +``` +class Solution { +public: + int searchInsert(vector& 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& vec) { + if (cur == NULL) return; + vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 +} +``` +中序遍历(左中右) +``` +void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 + traversal(cur->right, vec); // 右 +} +``` +中序遍历(中左右) +``` +void traversal(TreeNode* cur, vector& 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 preorderTraversal(TreeNode* root) { + vector result; + stack 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 inorderTraversal(TreeNode* root) { + vector result; // 存放中序遍历的元素 + stack 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 postorderTraversal(TreeNode* root) { + vector result; + stack 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> levelOrder(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + vector> result; + while (!que.empty()) { + int size = que.size(); + vector 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) diff --git a/video/.DS_Store b/video/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/video/.DS_Store differ diff --git a/video/0239.滑动窗口最大值.mp4 b/video/0239.滑动窗口最大值.mp4 deleted file mode 100644 index 3b984fb8..00000000 Binary files a/video/0239.滑动窗口最大值.mp4 and /dev/null differ diff --git a/video/239.滑动窗口最大值.gif b/video/239.滑动窗口最大值.gif new file mode 100644 index 00000000..90c8f11a Binary files /dev/null and b/video/239.滑动窗口最大值.gif differ diff --git a/video/239.滑动窗口最大值.mp4 b/video/239.滑动窗口最大值.mp4 new file mode 100644 index 00000000..5497d70d Binary files /dev/null and b/video/239.滑动窗口最大值.mp4 differ