1.题目描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
示例2:
输入:nums = [1], k = 1
输出:[1]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sliding-window-maximum
2.题目分析
本题的核心在于滑动窗口的保存与计算,我们如果直接采用暴力穷举进行求解的话,其复杂度会达到O(N*K)。其思想比较简单,这里就不做赘述。那么我们如何优化本题的解法呢?
2.1 定制化双向队列
我们要求的是滑动窗口的最大值,所以如果我们想不采用暴力穷举比较的方法的话,就要考虑利用滑动窗口的滑动二字的意义。
对于每个大小为k
的窗口,我们需要输出其最大值,但是数组是无序的,那么我们可以想到,如果在保存这个窗口的时候就将窗口内最大的放在特定位置(比如进行排序),这样每次窗口的输出是不是就可以直接取值了。问题又来了,窗口是滑动的,所以上一个窗口的最大值可能还是下一个窗口的最大值,甚至上一个窗口的最大值放到特定位置之后是否会对后续的窗口的最大值求解产生影响呢?所以我们在确保可以保存当前滑动窗口最大值的同时还要保证这个值的求解不会影响窗口间各个元素的相对位置。
假设我们每次都要维护一个窗口,根据题意这个窗口每次的滑动都会导致头部元素的删除、尾部元素的增加。这就跟数据结构中的双向队列deque
十分相似,所以我们试图使用deque
。但是很明显直接使用deque
的pop_front
和push_back
的话,我们还是要进行穷举处理。所以我们尝试一下在deque
的基础上进行将其push
和pop
进行定制化。
那么如何对我们所需要的队列类myQueue进行定制呢?
- 为了满足最大值的便捷提取,对于新的队列而言,其本身必须是一个经过类似排序处理的结构,使得当前窗口最大的元素在队列
首部
push
操作:为了避免排序对后续的操作产生难以估计的影响,我们采用如下流程进行数据的入队:- 遍历k个元素
- 当队列为空,当前元素入队;
- 当队列不为空,而且当前遍历到的元素大于
队列尾部元素
,就说明当前尾部元素绝对不可能是当前本窗口或者后续滑动后的窗口内最大元素(这一点可以自己举个例子验证一下),此时将尾部元素
出队,直到尾部元素大于等于当前元素。当前元素入队;
pop
操作:- 上述的push操作进行类排序操作并删除了一些元素,但是剩下的元素的相对位置是没有改变的
- 所以为了契合题目要求,我们在进行pop操作时需要一个value参数,判定当前首部元素是否还会在下一个窗口内,如果还会在就不操作,否则就真的将其删除
下面给出一个以数组[1,3,-1,-3,5,3]
的push的例子;
- 首先对于第一个窗口
[1,3,-1]
,首先处理元素1,直接入队
,然后遇到元素3,此时3>1(尾元素),所以1出队
,3入队;然后遇到-1,-1<3(尾元素),直接入队
- 对于第二个窗口
[3,-1,-3]
,此时后面两个元素均满足小于队尾元素
,直接入队 - 第三个窗口
[-1,-3,5]
,5要入队,5>-3,-3出队
;继续比较,5>-1,所以-1出队,5入队
- 以此类推可以得到最后一个窗口的队列是
[5,3]
根据上面的分析,类似于push操作,pop操作也需要根据当前窗口的值进行选择性pop,所以对于pop操作我们有如下规则:
如果此次窗口移除的元素value是单调队列的出口元素,也就是当前窗口的最左端,那么队列弹出该元素,否则不用任何操作
2.2 优先队列
上面的方法是基于已有的双向队列进行封装与改造,使得队列(窗口)内的元素变成相对有序的。这样就可以保证我们每次取队首元素时都是当前队列的最大值。
其实在C++的标准库中也内置了一种可以进行预排序的队列——优先队列 priority_queue
。优先队列的每个元素都具有特定的优先级,当我们进行队首元素的访问时,首先访问的是具有最高优先级的元素。这个优先级默认为最大的元素,也就是一个大顶堆。
但是我们直接使用优先队列的话貌似会有一些问题,举个栗子,对于上面的[1,3,-1,-3,5,3]
窗口大小为3的滑动窗口,前三个元素入队后,队首元素是3,但是在进行最大值输出后,这个队首元素是否该删除就成了一个问题。简单的优先队列存储并不能解决。于是我们考虑存储的时候对下标也进行存储来控制pop
行为是否要执行。这样我们就需要使用一个C++内置类型——pair。所以我们的优先队列初始化成了下面这个样子。其中pair
的k-v
对的含义是元素值-元素下标
priority_queue<pair<int,int>> que;
然后我们的解题思路就很简单了,
- 每次更新窗口添加新的值时,属于窗口的元素入队;
- 每次输出最大值时要先判断该值是否还在当前窗口(窗口下标范围为
[i-k+1,i]
)内,如果不在就要将其出队这里要注意当前窗口的含义。
3.题目解答
3.1 定制化双向队列
基于上面的分析,解题代码如下:
class Solution {
public:
class myQueue{
public:
// 定义单调队列
deque<int> que;
//如果push的数值大于当前队列尾部元素的数值,那么就将队列尾部端的数值弹出,
void push(int value){
while(!que.empty() && value>que.back()){
que.pop_back();
}
que.push_back(value);
}
//检查当前队首元素是否已经要滑出去
void pop(int value){
if(!que.empty() && value==que.front()){
que.pop_front();
}
}
int front(){
return que.front();
}
};
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
myQueue que;
vector<int> res;
//定义保存滑动窗口的队列
for(int i=0;i<k;i++){
que.push(nums[i]);
}
//第一次的队首元素必然是一个窗口最大值
res.push_back(que.front());
//窗口开始滑动,每次增加一个元素
for(int i=k;i<nums.size();i++){
que.pop(nums[i-k]);
que.push(nums[i]);
res.push_back(que.front());
}
return res;
}
};
3.2 优先队列解题
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
//定义一个优先队列存储当前的窗口内的值及其下标
priority_queue<pair<int,int>> que;
vector<int> res;
for(int i=0;i<k;i++){
//注意key与value的关系
que.emplace(nums[i],i);
}
// 此时的队首元素必然是第一个窗口的最大值
res.push_back(que.top().first);
for(int i=k;i<nums.size();i++){
//窗口尾部添加元素
que.emplace(nums[i],i);
//判断队首元素是否还在窗口内
while(que.top().second<i-k+1){
que.pop();
}
//队首元素是窗口内最大值
res.push_back(que.top().first);
}
return res;
}
总结:基于原始数据结构的定制化优化