滑动窗口最大值

滑动窗口最大值
题目链接
在这里插入图片描述

方法1:暴力求解:

对于每个滑动窗口,我们可以使用 O(k) 的时间遍历其中的每一个元素,找出其中的最大值。
对于长度为 n 的数组 nums ,窗口的数量为 n-k+1
因此该算法的时间复杂度为 O((n-k+1)k)=O(nk),会超出时间限制,需要进行一些优化。

暴力优化:
滑动窗口特点就是每次移动,会出一个元素,进一个元素。
也就是说它们共用着 k-1个元素,而只有 1 个元素是变化的。我们可以根据这个特点进行优化。

首先最容易想到的就是去维护一个最大值,窗口每滑动一次,我去更新这个最大值。显然只维护一个最大值的话,当最大值出去的时候,我需要找到第二大值来更新最大值。

那如果我维护来一个第二大值呢?同样,我还是需要去不断维护这个第二大值,就必然存在当第二大值出去或者成为第一大值的时候,我需要找到下一个第二大值,也就是第三大值,不难推理,有了第三大就还需要第四大…
就维护第一大第二大值为例,做了所有情况的预判,流程图如下:
在这里插入图片描述

标红的情况就是需要第三大值的情况。
综上可见:此路不通…

其实我们就是在解决这道题目的一个难点:如何维护一个最大值/如果用最短的时间找到最大值
上面我们企图通过保存特定值,来将找最大值的算法优化成常数时间,经过上面的分析,显然是不行的
而对于找最大值,最暴力的就是每次遍历全部元素需要o(n)的时间复杂度, 其实有点点像排序算法的优化,只不过我们这里只要最大值即可。如果能想到这里,我相信应该就很容易想到用堆这种数据结构,尝试用大顶堆来将这部分算法优化至o(lgn)

方法2:优先队列方法(大顶堆)

什么是优先队列呢?
优先队列就是不再遵循先入先出的原则,而是分为两种情况:
最大优先队列,无论入队顺序,当前最大的元素优先出队。
最小优先队列,无论入队顺序,当前最小的元素优先出队。
其实就是一个大顶堆/小顶堆实现的。显然这种数据结构,完美地帮我们解决了这道题目的一个难点:维护一个最大值

对应思路和算法:
初始时,我们将数组 nums 的前 k个元素放入优先队列(大顶堆)中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。

然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组nums 中的位置出现在滑动窗口左边界的左侧。因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其从优先队列中移除。
我们不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。
所以我们还需要保存元素的下标,才能让我们去判断它是否在滑动窗口内。
以[ 1,3,-1,-3,5,3,6,7]为例:
在这里插入图片描述

时间复杂度:O(nlogn),其中 n是数组nums 的长度。在最坏情况下,数组 nums 中的元素单调递增,那么最终优先队列中包含了所有元素,没有元素被移除。由于将一个元素放入优先队列的时间复杂度为 O(logn),因此总时间复杂度为 O(nlogn)。
空间复杂度:O(n),即为优先队列需要使用的空间。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
            public int compare(int[] pair1, int[] pair2) {
                return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1];
            }
        });
        for (int i = 0; i < k; ++i) {
            pq.offer(new int[]{nums[i], i});
        }
        int[] ans = new int[n - k + 1];
        ans[0] = pq.peek()[0];
        for (int i = k; i < n; ++i) {
            pq.offer(new int[]{nums[i], i});
            while (pq.peek()[1] <= i - k) {
                pq.poll();
            }
            ans[i - k + 1] = pq.peek()[0];
        }
        return ans;
    }
}

方法3:单调双端队列

什么是单调双端队列?
就是可以从队头出队,也可以从队尾出队,即不用遵循先进先出的规则,并且队列内的元素是有序的(从小到大/从大到小)

对应思路与算法:
1.首先把我们第一个窗口的所有值依次存入单调双端队列中,单调队列里面的值为单调递减的。

怎么保持队列元素的递减性呢?就是在元素入队的时候,如果发现队尾元素小于要加入的元素,则将队尾元素出队,直到队尾元素大于新元素时,再让新元素入队,目的就是维护一个单调递减的队列。

2.我们将第一个窗口的所有值,按照单调队列的规则入队之后,因为队列为单调递减,所以队头元素必为当前窗口的最大值,则将队头元素添加到结果数组中。

3.移动窗口,判断当前窗口的前一个元素是否和队头元素相等,如果相等则出队。

4.继续然后按照规则进行入队,维护单调递减队列。

5.判断对头元素是否在滑动窗口内,进行正确的出队操作后,将队头元素存到对应的结果数组里。

5.返回结果数组
在这里插入图片描述

public int[] maxSlidingWindow(int[] nums, int k) {
        int len = nums.length;
        if (len == 0) {
            return nums;
        }
        int[] arr = new int[len - k + 1];
        int arr_index = 0;
        //单调双向队列
        Deque<Integer> deque = new LinkedList<>();
        //第一个窗口的值按照规则入队
        for (int i = 0; i < k; i++) {
             while (!deque.isEmpty() && deque.peekLast() < nums[i]) {
               deque.removeLast();
            }
            deque.offerLast(nums[i]);
        }
        //存到数组里,队头元素
        arr[arr_index++] = deque.peekFirst();
        //移动窗口
        for (int j = k; j < len; j++) {
            //对应上面蓝色情况,移出队头元素
            if (nums[j - k] == deque.peekFirst()) {
                deque.removeFirst();
            }
            while (!deque.isEmpty() && deque.peekLast() < nums[j]) {
                deque.removeLast();
            }
            deque.offerLast(nums[j]);
            arr[arr_index++] = deque.peekFirst();
        }
        return arr;
    }

方法4:分块 + 预处理

思路与算法
首先分组。我们可以将数组 nums 从左到右按照 k 个一组进行分组,最后一组中元素的数量可能会不足 k 个。

如果我们希望求出 nums[i] 到 nums[i+k−1] 的最大值,就会有两种情况:
如果 i 是 k的倍数,那么nums[i] 到nums[i+k−1] 恰好是一个分组。我们只要预处理出每个分组中的最大值,即可得到答案;
如果 i不是 k的倍数,那么nums[i] 到 nums[i+k−1] 会跨越两个分组,

假设 j是 k 的倍数,并且满足 i < j ≤i+k−1,那么 nums[i] 到 nums[j−1] 就是第一个分组的后缀,nums[j] 到 nums[i+k−1] 就是第二个分组的前缀。(看图比较好理解)

如果我们能够预处理出每个分组中的前缀最大值以及后缀最大值,同样可以在 O(1)的时间得到答案。

在预处理完成之后,对于 nums[i] 到nums[i+k−1] 的所有元素,

如果 i不是 k 的倍数,那么窗口中的最大值为 suffixMax[i] 与 prefixMax[i+k−1] 中的较大值;

如果 i 是 k 的倍数,那么此时窗口恰好对应一整个分组,suffixMax[i] 和 prefixMax[i+k−1] 都等于分组中的最大值。

因此无论窗口属于哪一种情况,max{suffixMax[i],prefixMax[i+k−1]}都为滑动窗口最大值。

示例:

1:将8个元素按k分组,分为3组

2:计算出每组的suffixMax,prefixMax

3:开始遍历,无论i是否为k的整数倍,当前滑动窗口的max始终等于max{suffixMax[i],prefixMax[i+k−1]}
在这里插入图片描述时间复杂度:O(n),分别需要 O(n) 的时间预处理出数组 prefixMax,suffixMax 。
空间复杂度:O(n) prefixMax 和 suffixMax 需要的空间

public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int[] prefixMax = new int[n];
        int[] suffixMax = new int[n];
        for (int i = 0; i < n; ++i) {
            if (i % k == 0) {
                prefixMax[i] = nums[i];
            }
            else {
                prefixMax[i] = Math.max(prefixMax[i - 1], nums[i]);
            }
        }
        for (int i = n - 1; i >= 0; --i) {
            if (i == n - 1 || (i + 1) % k == 0) {
                suffixMax[i] = nums[i];
            } else {
                suffixMax[i] = Math.max(suffixMax[i + 1], nums[i]);
            }
        }

        int[] ans = new int[n - k + 1];
        for (int i = 0; i <= n - k; ++i) {
            ans[i] = Math.max(suffixMax[i], prefixMax[i + k - 1]);
        }
        return ans;
    }

  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值