mirror of
https://github.com/labuladong/fucking-algorithm.git
synced 2025-07-05 03:36:39 +08:00
Merge pull request #61 from warmingkkk/english
English 翻译数据结构系列/单调队列.md
This commit is contained in:
183
data_structure/Monotonic queue.md
Normal file
183
data_structure/Monotonic queue.md
Normal file
@ -0,0 +1,183 @@
|
||||
# special data structure: monotonic queue
|
||||
Author:labuladong https://github.com/labuladong
|
||||
|
||||
Translator:warmingkkk https://github.com/warmingkkk
|
||||
|
||||
The previous article talked about a special data structure "monotonic stack"a type of problem "Next Greater Number" is solved. This article writes a similar data structure "monotonic queue".
|
||||
|
||||
Maybe you haven't heard of the name of this data structure. In fact, it is not difficult. It is a "queue", but it uses a clever method to make the elements in the queue monotonically increase (or decrease). What's the use of this data structure? Can solve a series of problems with sliding windows.
|
||||
|
||||
See a LeetCode title,difficulty is hard:
|
||||
|
||||

|
||||
|
||||
### 1, build a problem solving framewor
|
||||
|
||||
This problem is not complicated. The difficulty is how to calculate the maximum value in each "window" at O(1) time, so that the entire algorithm is completed in linear time.We discussed similar scenarios before and came to a conclusion:
|
||||
|
||||
In a bunch of numbers,the best value is known,If you add a number to this bunch of numbers,you can quickly calculate the most value by comparing them,but if you reduce one number,you may not get the maximum vaue quickly,but you can have to go through all the numbers and find the maximum value again.
|
||||
|
||||
Back to the scenario of this problem,as each window advances,you need to add a number and decrease one number,so if you want to get a new maximum value in O(1) time,you need a special "monotonic queue" data structure to assist.
|
||||
|
||||
An ordinary queue must have these two operations:
|
||||
|
||||
```java
|
||||
class Queue {
|
||||
void push(int n);
|
||||
// or enqueue, adding element n to the end of the line
|
||||
void pop();
|
||||
// or dequeue, remove the leader element
|
||||
}
|
||||
```
|
||||
|
||||
The operation of a "monotonic queue" is similar:
|
||||
|
||||
```java
|
||||
class MonotonicQueue {
|
||||
// add element n to the end of the line
|
||||
void push(int n);
|
||||
// returns the maximum value in the current queue
|
||||
int max();
|
||||
// if the head element is n, delete it
|
||||
void pop(int n);
|
||||
}
|
||||
```
|
||||
Of course, the implementation methods of these APIs are definitely different from the general Queue, but we leave them alone, and think that the time complexity of these operations is O (1), first answer this "sliding window" problem Frame out:
|
||||
|
||||
```cpp
|
||||
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
|
||||
MonotonicQueue window;
|
||||
vector<int> res;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
if (i < k - 1) { // fill the first k-1 of the window first
|
||||
window.push(nums[i]);
|
||||
} else { // the window begins to slide forward
|
||||
window.push(nums[i]);
|
||||
res.push_back(window.max());
|
||||
window.pop(nums[i - k + 1]);
|
||||
// nums[i - k + 1] is the last element of the window
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
The idea is simple, understand? Below we start the highlight, the implementation of monotonic queues.
|
||||
|
||||
### 2, Implementing a monotonic queue data structure
|
||||
|
||||
First we need to know another data structure: deque, which is a double-ended queue. It's simple:
|
||||
|
||||
```java
|
||||
class deque {
|
||||
// insert element n at the head of the team
|
||||
void push_front(int n);
|
||||
// insert element n at the end of the line
|
||||
void push_back(int n);
|
||||
// remove elements at the head of the team
|
||||
void pop_front();
|
||||
// remove element at the end of the line
|
||||
void pop_back();
|
||||
// returns the team head element
|
||||
int front();
|
||||
// returns the tail element
|
||||
int back();
|
||||
}
|
||||
```
|
||||
|
||||
Moreover, the complexity of these operations is O (1). This is actually not a rare data structure. If you use a linked list as the underlying structure, it is easy to implement these functions.
|
||||
|
||||
The core idea of "monotonic queue" is similar to "monotonic stack". The push method of the monotonic queue still adds elements to the end of the queue, but deletes the previous elements smaller than the new element:
|
||||
|
||||
```cpp
|
||||
class MonotonicQueue {
|
||||
private:
|
||||
deque<int> data;
|
||||
public:
|
||||
void push(int n) {
|
||||
while (!data.empty() && data.back() < n)
|
||||
data.pop_back();
|
||||
data.push_back(n);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
As you can imagine, adding the size of the number represents the weight of the person, squashing the underweight in front, and stopping until it encounters a larger magnitude.
|
||||
|
||||

|
||||
|
||||
If every element is added like this, the size of the elements in the monotonic queue will eventually decrease in a monotonic order, so our max () API can be written like this:
|
||||
|
||||
```cpp
|
||||
int max() {
|
||||
return data.front();
|
||||
}
|
||||
```
|
||||
|
||||
The pop () API deletes element n at the head of the queue, which is also very easy to write:
|
||||
|
||||
```cpp
|
||||
void pop(int n) {
|
||||
if (!data.empty() && data.front() == n)
|
||||
data.pop_front();
|
||||
}
|
||||
```
|
||||
|
||||
The reason to judge `data.front () == n` is because the queue head element n we want to delete may have been" squashed ", so we don't need to delete it at this time:
|
||||
|
||||

|
||||
|
||||
At this point, the monotonous queue design is complete, look at the complete problem-solving code:
|
||||
|
||||
```cpp
|
||||
class MonotonicQueue {
|
||||
private:
|
||||
deque<int> data;
|
||||
public:
|
||||
void push(int n) {
|
||||
while (!data.empty() && data.back() < n)
|
||||
data.pop_back();
|
||||
data.push_back(n);
|
||||
}
|
||||
|
||||
int max() { return data.front(); }
|
||||
|
||||
void pop(int n) {
|
||||
if (!data.empty() && data.front() == n)
|
||||
data.pop_front();
|
||||
}
|
||||
};
|
||||
|
||||
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
|
||||
MonotonicQueue window;
|
||||
vector<int> res;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
if (i < k - 1) { // fill the first k-1 of the window first
|
||||
window.push(nums[i]);
|
||||
} else { // window slide forward
|
||||
window.push(nums[i]);
|
||||
res.push_back(window.max());
|
||||
window.pop(nums[i - k + 1]);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
### 3, Algorithm complexity analysis
|
||||
|
||||
Readers may be wondering, while the push operation contains a while loop, the time complexity is not O (1), so the time complexity of this algorithm should not be linear time, right?
|
||||
|
||||
The complexity of the push operation alone is not O (1), but the overall complexity of the algorithm is still O (N) linear time. To think of it this way, each element in nums is pushed_back and pop_back at most once, without any redundant operations, so the overall complexity is still O (N).
|
||||
|
||||
The space complexity is very simple, which is the size of the window O (k).
|
||||
|
||||
### 4, Final conclusion
|
||||
|
||||
Some readers may think that "monotonic queues" and "priority queues" are more similar, but they are actually very different.
|
||||
|
||||
The monotonic queue maintains the monotonicity of the queue by deleting elements when adding elements, which is equivalent to extracting the monotonically increasing (or decreasing) part of a function; while the priority queue (binary heap) is equivalent to automatic sorting, the difference is large went.
|
||||
|
||||
Hurry up and get LeetCode's Question 239 ~
|
@ -1,185 +0,0 @@
|
||||
# 特殊数据结构:单调队列
|
||||
|
||||
前文讲了一种特殊的数据结构「单调栈」monotonic stack,解决了一类问题「Next Greater Number」,本文写一个类似的数据结构「单调队列」。
|
||||
|
||||
也许这种数据结构的名字你没听过,其实没啥难的,就是一个「队列」,只是使用了一点巧妙的方法,使得队列中的元素单调递增(或递减)。这个数据结构有什么用?可以解决滑动窗口的一系列问题。
|
||||
|
||||
看一道 LeetCode 题目,难度 hard:
|
||||
|
||||

|
||||
|
||||
### 一、搭建解题框架
|
||||
|
||||
这道题不复杂,难点在于如何在 O(1) 时间算出每个「窗口」中的最大值,使得整个算法在线性时间完成。在之前我们探讨过类似的场景,得到一个结论:
|
||||
|
||||
在一堆数字中,已知最值,如果给这堆数添加一个数,那么比较一下就可以很快算出最值;但如果减少一个数,就不一定能很快得到最值了,而要遍历所有数重新找最值。
|
||||
|
||||
回到这道题的场景,每个窗口前进的时候,要添加一个数同时减少一个数,所以想在 O(1) 的时间得出新的最值,就需要「单调队列」这种特殊的数据结构来辅助了。
|
||||
|
||||
一个普通的队列一定有这两个操作:
|
||||
|
||||
```java
|
||||
class Queue {
|
||||
void push(int n);
|
||||
// 或 enqueue,在队尾加入元素 n
|
||||
void pop();
|
||||
// 或 dequeue,删除队头元素
|
||||
}
|
||||
```
|
||||
|
||||
一个「单调队列」的操作也差不多:
|
||||
|
||||
```java
|
||||
class MonotonicQueue {
|
||||
// 在队尾添加元素 n
|
||||
void push(int n);
|
||||
// 返回当前队列中的最大值
|
||||
int max();
|
||||
// 队头元素如果是 n,删除它
|
||||
void pop(int n);
|
||||
}
|
||||
```
|
||||
|
||||
当然,这几个 API 的实现方法肯定跟一般的 Queue 不一样,不过我们暂且不管,而且认为这几个操作的时间复杂度都是 O(1),先把这道「滑动窗口」问题的解答框架搭出来:
|
||||
|
||||
```cpp
|
||||
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
|
||||
MonotonicQueue window;
|
||||
vector<int> res;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
if (i < k - 1) { //先把窗口的前 k - 1 填满
|
||||
window.push(nums[i]);
|
||||
} else { // 窗口开始向前滑动
|
||||
window.push(nums[i]);
|
||||
res.push_back(window.max());
|
||||
window.pop(nums[i - k + 1]);
|
||||
// nums[i - k + 1] 就是窗口最后的元素
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
这个思路很简单,能理解吧?下面我们开始重头戏,单调队列的实现。
|
||||
|
||||
### 二、实现单调队列数据结构
|
||||
|
||||
首先我们要认识另一种数据结构:deque,即双端队列。很简单:
|
||||
|
||||
```java
|
||||
class deque {
|
||||
// 在队头插入元素 n
|
||||
void push_front(int n);
|
||||
// 在队尾插入元素 n
|
||||
void push_back(int n);
|
||||
// 在队头删除元素
|
||||
void pop_front();
|
||||
// 在队尾删除元素
|
||||
void pop_back();
|
||||
// 返回队头元素
|
||||
int front();
|
||||
// 返回队尾元素
|
||||
int back();
|
||||
}
|
||||
```
|
||||
|
||||
而且,这些操作的复杂度都是 O(1)。这其实不是啥稀奇的数据结构,用链表作为底层结构的话,很容易实现这些功能。
|
||||
|
||||
「单调队列」的核心思路和「单调栈」类似。单调队列的 push 方法依然在队尾添加元素,但是要把前面比新元素小的元素都删掉:
|
||||
|
||||
```cpp
|
||||
class MonotonicQueue {
|
||||
private:
|
||||
deque<int> data;
|
||||
public:
|
||||
void push(int n) {
|
||||
while (!data.empty() && data.back() < n)
|
||||
data.pop_back();
|
||||
data.push_back(n);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
你可以想象,加入数字的大小代表人的体重,把前面体重不足的都压扁了,直到遇到更大的量级才停住。
|
||||
|
||||

|
||||
|
||||
如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个单调递减的顺序,因此我们的 max() API 可以可以这样写:
|
||||
|
||||
```cpp
|
||||
int max() {
|
||||
return data.front();
|
||||
}
|
||||
```
|
||||
|
||||
pop() API 在队头删除元素 n,也很好写:
|
||||
|
||||
```cpp
|
||||
void pop(int n) {
|
||||
if (!data.empty() && data.front() == n)
|
||||
data.pop_front();
|
||||
}
|
||||
```
|
||||
|
||||
之所以要判断 `data.front() == n`,是因为我们想删除的队头元素 n 可能已经被「压扁」了,这时候就不用删除了:
|
||||
|
||||

|
||||
|
||||
至此,单调队列设计完毕,看下完整的解题代码:
|
||||
|
||||
```cpp
|
||||
class MonotonicQueue {
|
||||
private:
|
||||
deque<int> data;
|
||||
public:
|
||||
void push(int n) {
|
||||
while (!data.empty() && data.back() < n)
|
||||
data.pop_back();
|
||||
data.push_back(n);
|
||||
}
|
||||
|
||||
int max() { return data.front(); }
|
||||
|
||||
void pop(int n) {
|
||||
if (!data.empty() && data.front() == n)
|
||||
data.pop_front();
|
||||
}
|
||||
};
|
||||
|
||||
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
|
||||
MonotonicQueue window;
|
||||
vector<int> res;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
if (i < k - 1) { //先填满窗口的前 k - 1
|
||||
window.push(nums[i]);
|
||||
} else { // 窗口向前滑动
|
||||
window.push(nums[i]);
|
||||
res.push_back(window.max());
|
||||
window.pop(nums[i - k + 1]);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
**三、算法复杂度分析**
|
||||
|
||||
读者可能疑惑,push 操作中含有 while 循环,时间复杂度不是 O(1) 呀,那么本算法的时间复杂度应该不是线性时间吧?
|
||||
|
||||
单独看 push 操作的复杂度确实不是 O(1),但是算法整体的复杂度依然是 O(N) 线性时间。要这样想,nums 中的每个元素最多被 push_back 和 pop_back 一次,没有任何多余操作,所以整体的复杂度还是 O(N)。
|
||||
|
||||
空间复杂度就很简单了,就是窗口的大小 O(k)。
|
||||
|
||||
**四、最后总结**
|
||||
|
||||
有的读者可能觉得「单调队列」和「优先级队列」比较像,实际上差别很大的。
|
||||
|
||||
单调队列在添加元素的时候靠删除元素保持队列的单调性,相当于抽取出某个函数中单调递增(或递减)的部分;而优先级队列(二叉堆)相当于自动排序,差别大了去了。
|
||||
|
||||
赶紧去拿下 LeetCode 第 239 道题吧~
|
||||
|
||||
坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章:
|
||||
|
||||

|
Binary file not shown.
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 49 KiB |
Reference in New Issue
Block a user