Bootstrap

滑动窗口的最大值问题

今天解决一道算法中的滑动窗口问题,依次给出几种解决思路。

目录

题目描述

解题思路

方法一:暴力解法

方法二:辅助队列

方法三:大顶堆法


题目描述

给定一个数组 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 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ nums.length。

解题思路

方法一:暴力解法

最“原始”的方法是针对每一个滑动窗口,求其中的最大值,然后保存到结果数组中。该种方法总共需要遍历n-k+1趟,每趟在找最大元素的时候只需遍历k个元素,求窗口的最大值。该方法需要明确循环时候的边界,这种方法会超时。

public int[] maxSlidingWindow(int[] nums, int k) {
    if(nums == null || nums.length == 0) {
        return null;
    }
    int n = nums.length;
    int[] ret = new int[n-k+1];
    for(int i = 0; i <= n-k; ++i) {
        int max = nums[i];
        for(int j = i+1; j < i+k; ++j) {
            max = Math.max(nums[j], max);
        }
        ret[i] = max;
    }
    return ret;
}

复杂度分析:时间复杂度为O(n*k),因为外层遍历的躺数取决于n的大小,内层每次对k个元素比较找最大值。空间复杂度为O(n),需要使用一个n-k+1大的数组来保存结果。

方法二:辅助队列

前面这种方法在遍历下一个元素时,需要不断比较k个元素的最大值,显示是最“笨”也是最容易想到的方法,但有没有一种方法,在遍历下一个元素时,就能直接获取到当前滑动窗口中的最大值,答案是肯定的。我们使用一个辅助队列来存储到目前元素为止,可能成为窗口中最大元素的下标,如果要获取当前窗口的最大值,则取队首元素对应的值即可。那么,怎么样存储元素下标呢?每遍历一个新元素,将该元素和队尾元素值比较,如果大于队尾元素,则将队尾元素值出队,并重复该比较过程,知道队列为空或者当前元素小于队尾元素为止,之后将当前元素下标加入队尾,因为如果队列中最大值出队后,当前更小的元素可能会成为窗口中的最大值。把这个过程拆分成两段来写,小于k的为一段,大于等于k的为另一段,因为在小于k时,只需要求得当前窗口的最大值情况,而大于等于k时,需要和队首元素做差值比较,判断当前元素下标和最大值下标差是否超过k(包括等于k),如果超过,则将最大值下标出队。

public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums == null || nums.length == 0 || k == 0)
            return new int[0];
        int n = nums.length;
        // 使用双端队列,方便收尾进出
        LinkedList<Integer> indexQueue = new LinkedList<>();
        indexQueue.offer(0);
        //1.先找k内的最大值
        for(int i = 1; i < k; ++i) {
            //把有可能成为窗口中最大值的元素下标入队
            while(!indexQueue.isEmpty() && nums[i] > nums[indexQueue.getLast()]) {
                indexQueue.removeLast();
            }
            indexQueue.addLast(i);
        }
        LinkedList<Integer> res = new LinkedList<>();
        res.add(nums[indexQueue.getFirst()]);
        for(int i = k; i < n; ++i) {
            while(!indexQueue.isEmpty() && nums[i] > nums[indexQueue.getLast()]) {
                indexQueue.removeLast();
            }
            indexQueue.addLast(i);
            if(i - indexQueue.getFirst() >= k) {
                indexQueue.removeFirst();
            }
            res.add(nums[indexQueue.getFirst()]);
        }

        int[] result = new int[res.size()];
        for(int i = 0; i < result.length; ++i) {
            result[i] = res.removeFirst();
        }
        return result;
    }

注意:这里的res可以定义为ArrayList,也可以定义为LinkedList,如果使用LinkedList,在最后返回数组的时候,就不能简单的使用res.get(i)的方法获取第i个元素,会出现超时的问题。因为LinkedList底层是使用链表存储的,不能随机访问第i个元素,只能采用获取队头元素的方式,相比而言,如果使用ArrayList则可以使用get(i)的方式。

复杂度分析:时间复杂度:只需要从头到尾遍历一遍数组即可,总体时间复杂度为O(n);空间复杂度:需要额外的空间来存放可能成为最大值的元素,最坏情况的空间复杂度为O(n)。

方法三:大顶堆法

使用大顶推维护队列的最大值,在窗口向前滑动的过程中,需要判断最大值有没有滑出窗口,如果当前元素下标减去大顶堆中最大值的下标超过k,则说明最大值已经滑出窗口,需要从大顶堆中移除最大值,然后获取堆顶元素。大顶堆中既要存放元素值,又要存放元素下标,可以使用仅保存两个元素的数组作为大顶堆中的数据结构。最后,在向前遍历元素的时候,只需要将堆顶元素加入到结果数组中。

public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums == null || nums.length == 0) {
            return null;
        }
        int n = nums.length;
        if(n < k)
            return null;
        PriorityQueue<int[]> queue = new PriorityQueue<>(new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0] != o2[0] ? o2[0] - o1[0] : o2[1] - o1[1];
            }
        });

        for(int i = 0; i < k; ++i) {
            queue.offer(new int[]{nums[i], i});
        }
        int[] ret = new int[n-k+1];
        ret[0] = queue.peek()[0];
        int j = 1;
        for(int i = k; i < n; ++i) {
            queue.offer(new int[]{nums[i], i});
            while(i-queue.peek()[1] >= k) {
                queue.poll();
            }
            ret[j] = queue.peek()[0];
            ++j;
        }
        return ret;
    }

复杂度分析:时间复杂度:将元素放入队列中的时间复杂度为O(logn),总共n个元素,因此总的时间复杂度为O(nlogn),空间复杂度为O(n),为大顶堆所使用的空间。

总结

在力扣的官方题解中,还给出一种分块+预处理的思路,但这种方法不容易想到。个人觉得,用第二种辅助队列的方法是比较容易处理的,虽然大顶堆的方法比较容易想到,但针对这道题目,如果仅维护元素值,在大顶堆中删除元素的时候会出现超时的情况,所以要加上元素下标值。综上,解决该题,优先考虑辅助队列方式。

;