问题描述:
题目链接:
滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
就像下面这样(图片来自力扣)
怎么办?
假设,有这么一个队列。
队列是有序的,而且,随着滑动窗口的移动,窗口内的队列始终有序(递减)。
那我们是不是只要每次滑动窗口移动以后返回窗口内队列的队头元素就可以啦!!!
简直很舒服。
所以,现在核心问题是:怎样才能让窗口里那个队列始终保持队列单调呢?
那什么时候队列的单调性可能会改变呢?
很明显,当滑动窗口移动后,最队头元素出去了,队尾按理说要添加进来一个元素。
但是,这个元素加入到对尾是不是就不递减了呢?
这个真说不来。但是我们可以定义一个新的push()入队函数,保证入队以后,队列还是递减的就行啦。
下面看入队且能保证队列单减的方法。
入队push()的原则
若push的数值大于入口(队尾)的元素值,
那么就直接将队列后端(队尾)的数值弹出。
如果一直大于,就一直弹对尾元素;
直到要push的数值比对尾元素的值小。(一个是最大的,一个是次大的)
出队pop()的原则
每次弹出的时候,
检查当前窗口要移除的元素value **(就是窗口最左边的元素)**和队列出口(队头)元素的大小,
若两者相等(要弹出的元素和队头元素)
那么,弹出。
出队这里要注意:并不一定每次往后移动都有元素弹出去的,至于为什么,可以跟着下面的例子模拟一下就能知道了。
其实只有一种情况会使用下面这个pop函数出队:那就是队头元素,恰好也在滑动窗口的最左边。
因为有的时候最大值在中间,那可能窗口左边的值在中间最大值进队的时候就已经删除了,所以这时候是没必要再做删除操作的。
举个例子:
比如窗口我空中括号表示,队列我用圆括号表示;
[ , , (5,4)]
上面这个窗口里有4个元素,但是队列里只有2个元素,这时候窗口往右边移动,次要删除队列左边的5吗?
明显不需要。
因为窗口下次移动的时候,窗口最左边的元素已经弹出去了,所以直接移动就行了。
但是,如果是下面这种情况就要出队了:
[(5, 4), , ]
语言描述令人头昏脑涨,还是举个例子模拟一下吧。模拟完了再来看上面的描述。
模拟:
数组:nums = [1,3,-1,-3,5,3,6,7]
滑动窗口:k = 3
滑动窗口丛0个元素开始添加元素,滑动窗口的最大值是从窗口里有3个元素以后开始算的。
所以滑动窗口第一个最大值是第三行的3,前两行不算。
前两行只是为了模拟代码执行的流程。
要push的值 | 因为push而弹出的值 | 队列内的元素(左队头右队尾) | 滑动窗口最左侧值 |
---|---|---|---|
1 | 无 | 1 | 1 |
3 | 1 | 3 | 1 |
-1 | 无 | 3,-1 | -1 |
-3 | 无 | 3,-1,-3 | 3 |
5 | 3,-1,-3 | 5 | -1 |
3 | 无 | 5,3 | -3 |
6 | 5,3 | 6 | 5 |
7 | 6 | 7 | 3 |
由上面的模拟可以看出,队列内队头元素始终是滑动窗口的最大值
难点:
要分清楚 滑动窗口里的元素
和
单调队列里元素的关系
- 单调队列的元素肯定在滑动窗口里,但是,是滑动窗口的子集
- 滑动窗口只保留最大的几个元素,肯定会保留这个窗口里的最大元素,至于还能保留几个其他元素,这个要看新添加进来的元素的大小,通过push函数,就可以把小于新添加元素的队内其他元素全部删除。
若新添加的元素足够大,那队内显然只会剩下他一个,他也就是队头 - 队列往右边移动的时候,也非常关键
- 若,此时滑动窗口最左边的元素恰好就是队头元素(即队列最大元素),那就得删除
总之,push函数就保证了,在这个滑动窗口里,单调队列的队头就是最大元素
pop函数保证了,滑动窗口正常移动
二者双剑合璧,找出窗口里最大的值
下面是代码实现
#include <string>
#include <queue>
#include <iostream>
using namespace std;
class Solution {
private:
class MyQueue{
//自己定义一个单调队列的专有操作
public:
//使用deque来实现单调队列
deque<int> que;
/*1.pop(value)弹出函数:
*什么情况下才能弹出呢?
*当滑动窗口最左边的元素 和
*单调队列最左边的元素(也就是队头元素)相等的时候,弹出。
*否则,就不做任何操作,不弹出任何东西。
*当然,当点掉队列不为空的时候,也是没法弹出的。
*/
void pop(int value) {
//front()函数:取队列的队头元素
if (!que.empty() && value == que.front()) { //弹出队头元素
que.pop_front();
}
}
//2.push(value)元素进队函数
/*什么情况下可以进入队列呢?
*进入队列要进行那些操作才能保持单调队列的单调减少呢?
*如果,将要进入队列的元素,大于当前的对尾元素,
*则pop_back(),把队的最后一个元素弹走。
*继续判断,直到队尾的最后一个元素大于要加入的元素,或者队空了,则结束。
*这样就可以保证,队里有最大的数,而且,这个数在队头,队头右边的数也是次大的。
*保证了单调队列的单调性。
*/
void push(int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
}
//把比value小的弹出完以后,将valua放入队列中。
que.push_back(value);
}
//最后,队头元素,这个元素,就是这个滑动窗口里最大的那个仔
int front() {
return que.front();
}
}
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
for (int i = 0;i < k; i++) {
que.qush(nums[i]);
}
//result记录前k的元素的最大值
result.push_back(que.front());
for (int i = k; i <nums.size(); i++) {
//注意,这里删除元素和加入元素的顺序不能反了,先加入pop弹出元素再push加入元素
que.pop(nums[i - k]);//滑动窗口移除最前面元素
que.push(nums[i]);//滑动窗口加上后面对应的元素
result.push_back(que.front());//记录对应的最大值
}
return result;
}
};