滑动窗口的最大值——两种方法~

222 阅读5分钟

要求:

给定一个数组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

思路1:

先引入介绍一个此类方法常用的思路:单调双向队列。双向队列的意思就是既可以在头部插入删除,又可以在尾部插入删除,单调就是指队列存储的元素是依次递减或递增的。此处借用一张图以示说明:

头部                      尾部
 --------------------------
|  5   3   2   1   0  -1   |
 --------------------------
  由大     →       到小

我们一直要使用这个单调的双向队列来做判断。 说是双向队列其实是用LinkedList实现的,因为LinkedList有添加,删除队首队尾元素等方法。 每次窗口滑动的时候需要做两个判断:

  1. 之前的最大值是不是已经不在窗口范围里了
  2. 新加进来的值是不是比之前的最大值要来的大
  • 如果刚进来的元素比队列尾部元素大,那么先将队列尾部的元素弹出,把刚进来的元素添加到队列尾部。这个操作需要循环操作,只要队尾元素小就要弹出,以此来保证队列元素的单调性。
  • 若刚进来的元素比队列的队尾元素小,那么将元素直接添加到队列尾部即可。

演示过程:

添加元素

假设要进入队列的元素是5、4、1、2、6。第一次队列为空,所以直接进入即可。元素5,索引为0.

头部                    尾部
 --------------------------
|  5                     |
|  ↑                     |
|  0                     |
 --------------------------
  大                  小

第二次进入队列的是元素4,索引为1,需要与5比较后再进。比5小,可以进。

头部                    尾部
 --------------------------
|  5    4                |
|  ↑    ↑                |
|  0    1                |
 --------------------------
  大                  小

第三次进入队列的是元素1,索引为2,需要与4比较后再进。比4小,可以进。

头部                    尾部
 --------------------------
|  5    4    1            |
|  ↑    ↑    ↑            |
|  0    1    2            |
 --------------------------
  大                  小

第四次进入队列的是元素2,索引为3,需要与1比较后再进。比1大,所以需要丢掉队尾元素1,才能让2进。

头部                    尾部
 --------------------------
|  5    4    2            |
|  ↑    ↑    ↑            |
|  0    1    3            |
 --------------------------
  大                  小

每次滑动窗口,其实就是每次添加新元素进去,都可以取一个最大值出来。 最后,来了索引为4的元素6,比里面的所有元素都大,就需要弹出所有元素,让6进队。

头部                    尾部
 --------------------------
|  6                      |
|  ↑                      |
|  5                      |
 --------------------------
  大                  小

删除元素

例如:窗口大小为2,此前窗口包含[5,4],现在窗口是[4,1],需要删除5,让4成为队首元素。

元素: 5  [4  1]  2
索引: 0   1  2   3

头部                    尾部
 --------------------------
|  5   4   1              |
|  ↑   ↑   ↑              |
|  0   1   2              |
 --------------------------
  大                  小

删除元素后:

头部                    尾部
 --------------------------
|     4   1               |
|     ↑   ↑                |
|     1   2               |
 --------------------------
  大                  小

注意事项:

  • 我们存储进队列,进行比较的是索引,并不是具体的值,因为双向队列是靠LinkedList实现的,提供的是数组,有下标,队列里的元素是没有下标的,所以我们一直用索引记录数字,这样好从数组里取数字。
  • 然后当窗口滑动的时候,需要检查当前的最大元素的索引是否还在窗口范围内。
  • 新要入队的元素只要大小等于队尾元素,都需要进行替换
  • 不需要特别的去为了前面几个单独的数字做比较,只需要判断窗口有没有形成,只有形成了大小为k的窗口,才开始收集窗口内的最大值。

代码:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || k < 1 || nums.length < k) {
            return new int[0];
        }

        int index = 0;
        int[] res = new int[nums.length - k + 1];
        LinkedList<Integer> qMax = new LinkedList<>();

        for (int i = 0; i < nums.length; i++) {
            // 在队列不为空的情况下,如果队列尾部的元素要比当前的元素小,或等于当前的元素
            // 那么为了维持从大到小的原则,我必须让尾部元素弹出
            while (!qMax.isEmpty() && nums[qMax.peekLast()] <= nums[i]) {
                qMax.pollLast();
            }
            // 不走 while 的话,说明我们正常在队列尾部添加元素
            qMax.addLast(i);
            // 如果滑动窗口已经略过了队列中头部的元素,则将头部元素弹出
            if (qMax.peekFirst() == (i - k)) {
                qMax.pollFirst();
            }
            // 看看窗口有没有形成,只有形成了大小为 k 的窗口,我才能收集窗口内的最大值
            if (i >= (k - 1)) {
                res[index++] = nums[qMax.peekFirst()];
            }
        }
        return res;
    }
}

方法二:优先队列

还是这种抖机灵的做法,先往里添加元素。重写优先队列,使得它改为按从大到小排列,而优先队列有一个很巧的方法是remove()指定元素。这样我们不需要像上面那样在一个数组中保存索引了。

代码:

import java.util.*;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size){
        ArrayList<Integer> res = new ArrayList<Integer>();
        if (size > num.length || size <= 0) return res;
        PriorityQueue<Integer> heap = new PriorityQueue<Integer>(15,new Comparator<Integer>(){
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        for (int i = 0; i < size; i++) {
            heap.add(num[i]);
        }
        res.add(heap.peek());
        for (int i = 0; i < num.length - size ; i++) {
            heap.remove(num[i]);
            heap.add(num[size + i ]);
            res.add(heap.peek());
        }
        return res;
    }
}

个人感觉优先队列是一个很好的工具,然后操作的步骤自己也可以想得到,就好比是java顺着我们的心意给我们的一个工具。单调双向队列,就也很常用,需要学习一下,然后后面也会经常用到的那种。