给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
本题可以采用暴力方法求解,每次滑动窗口所移动到的k个数中求最大值,那么每次需要O(k)个时间,一共n步,则O(nk)
当然也可以用单调队列进行求解,单调队列与单调栈问题相类似,单调栈问题参看包含min函数的栈(单调栈)
单调队列与单调栈问题类似,我们可以观察队列里面是否有一些元素是没有用的,我们把这些没有用的元素去掉的话,看看是否会得到单调性。
如上图为例,假设我们每次求的是滑动窗口中的最小值,当-3进来之后,第一个3肯定没有用,我们每次求的是队列中最小值,-3小于3,3是在-3的左边,所以说,这个3会被先弹出去,换句话说,只要-3在,3就永远不会被当成最小值输出,并且-3还活的更久一点,它会在3被移出滑动窗口之后才会被移出去,因此我们就可以断定前面的3一定不会被当成答案输出出来,就可以去掉。同样,-1也是如此。
因此,只要队列里面存在前面一个数比后面的数还要大,那么前面的数就肯定没有用,因为后面的数会后被弹出来,而且更小,因此,只要有这样的逆序对的话,我们就可以把大的点删掉,我们把所有这样的数都删掉,整个队列就会变成严格单调上升的队列了。
我们要求队列中的最小值,那么严格单调上升的队列就是队头元素,所以每次找最小值的时候,直接找队头元素即可。
总结:单调栈和单调队列的问题,我们可以先考虑用栈和队列来暴力的模拟原来的问题,即常规思路,然后再看看常规思路里面,栈和队列中哪些元素是没有用的,然后再删掉,看看剩下的元素是否有单调性,如果剩下的元素有单调性,就可以做一些优化,如取极值或者二分。
而对于本题而言:
由于我们需要求出的是滑动窗口的最大值,如果当前的滑动窗口中有两个下标 i 和 j,其中 i 在 j 的左侧(i < j),并且 i对应的元素不大于 j对应的元素即(nums[i]<=nums[j]),那么会发生什么呢?
当滑动窗口向右移动时,只要 i 还在窗口中,那么 j 一定也还在窗口中,这是 i 在 j的左侧所保证的。因此,由于nums[j] 的存在,nums[i] 一定不会是滑动窗口中的最大值了,我们可以将nums[i] 永久地移除
因此我们可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组 nums 中对应的值是严格单调递减的。因为如果队列中有两个相邻的下标,它们对应的值相等或者递增,那么令前者为 i,后者为 j,就对应了上面所说的情况,即nums[i] 会被移除,这就产生了矛盾。
当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果前者大于等于后者,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。
**由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。**但与方法一中相同的是,此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。
为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」。
在Java中,双端队列及其应用可以参考Java双端队列Deque及其应用
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int len=nums.length;
if(len<1){
return nums;
}
//创建双端队列,使用LinkedList实现类
//注意,双端队列存储的是元素的下标
Deque<Integer> queue=new LinkedList<Integer>();
List<Integer> list=new ArrayList<Integer>(); //用于保存最终结果
int st=0,ed=k-1; //st,ed分别指向滑动窗口的左右边界
//我们先把初始情况下滑动窗口里面的元素都加入到单调队列中,这里隐含了k>=len的问题
for(int i=st;i<=ed;i++){
//当滑动窗口右移时,会不断判断当前元素是否大于或者等于队尾元素对应的值,如果满足条件的话,就移出队尾元素
//一直到队列为空或者当前元素小于队尾元素对应的值为止
//才把当前元素的下标加入到队尾,这样可以保持队列的单调递减的性质
while(!queue.isEmpty() && nums[i]>=nums[queue.peekLast()]){
int temp=queue.pollLast();
}
queue.offerLast(i);
}
//把队头元素加入到结果集中,队头元素对应的值即滑动窗口的最大值
list.add(nums[queue.peekFirst()]);
while(ed<len-1){
//滑动窗口后移一位
st++;
ed++;
//当队列不为空,并且队头元素(下标)处在滑动窗口左端点的左边时,就需要把它移出队列
if(!queue.isEmpty() && queue.peekFirst()<st){
int temp=queue.pollFirst();
}
//当滑动窗口右移时,会不断判断当前元素是否大于或者等于队尾元素对应的值,如果满足条件的话,就移出队尾元素
//一直到队列为空或者当前元素小于队尾元素对应的值为止
//才把当前元素的下标加入到队尾,这样可以保持队列的单调递减的性质
while(!queue.isEmpty() && nums[ed]>=nums[queue.peekLast()]){
int temp=queue.pollLast();
}
queue.offerLast(ed);
list.add(nums[queue.peekFirst()]);
}
//把结果存入到数组中
int size=list.size();
int res[]=new int[size];
for(int i=0;i<size;i++){
res[i]=list.get(i);
}
return res;
}
}