LeetCode239. 滑动窗口最大值解法心得:单调队列的奥秘

一、题目描述:滑动窗口最大值问题

给定一个整数数组 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
  • 依此类推…

二、暴力解法的痛点

最容易想到的方法是遍历每个窗口,每次用 O(k) 时间找出窗口内的最大值。
但这样的时间复杂度是 O(nk),当数组长度 n 和窗口大小 k 很大时,会超时。
核心痛点:每次窗口滑动后,我们都重新比较了整个窗口内的所有元素,浪费了大量重复计算。
例如,窗口从 [1,3,-1] 滑动到 [3,-1,-3] 时,3-1 这两个元素的大小关系已经比较过了,但暴力解法还是会重新比较。

三、单调队列解法:用队列维护窗口最大值

核心思路:用队列存储元素下标,严格保持队列单调递减

我们可以用一个双端队列(Deque)来存储元素的下标,队列中的下标对应的元素值严格单调递减。
这样,队列的队首元素始终是当前窗口的最大值。
关键操作

  1. 剔除超出窗口边界的元素:当队列中的下标超出当前窗口的左边界时,将其从队首弹出。
  2. 剔除小于最新元素的元素:在插入新元素前,从队尾弹出所有比新元素小的元素,确保队列单调递减。
  3. 队列存储索引下标:通过下标判断元素是否在当前窗口内,避免越界。
  4. 严格单调递减:队列中的元素值从队首到队尾严格递减,确保队首始终是最大值。

代码实现:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || k <= 0) return new int[0];
        int n = nums.length;
        int[] res = new int[n - k + 1];
        Deque<Integer> deque = new ArrayDeque<>();

        for (int i = 0; i < n; i++) {
            // 1. 剔除超出窗口边界的元素
            // 当队列中的下标小于当前窗口的左边界(i - k + 1)时,从队首弹出
            while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
                deque.pollFirst();
            }

            // 2. 剔除小于最新元素的元素
            // 从队尾弹出所有比当前元素小的下标,确保队列单调递减
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }

            // 3. 新元素入队(存储下标)
            deque.offerLast(i);

            // 4. 当窗口形成后,记录队首元素(即当前窗口的最大值)
            if (i >= k - 1) {
                res[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        return res;
    }
}

四、单调队列的奥秘:为什么这样做可行?

1. 为什么要剔除超出窗口边界的元素?

队列中存储的是元素的下标。当窗口滑动时,左侧的元素会被移出窗口。
如果队列中的下标小于当前窗口的左边界(i - k + 1),说明该元素已经不在当前窗口内,需要从队首弹出。

2. 为什么要剔除小于最新元素的元素?

当新元素加入窗口时,如果队列中存在比新元素小的元素,那么这些元素在新元素存在的情况下,永远不可能成为窗口的最大值。
例如,窗口 [3,1,2] 中,新元素 2 加入时,队列中已有 [3,1](下标)。由于 12 小,且 2 在窗口中的位置比 1 更靠右,因此 1 永远不可能成为窗口的最大值,所以将其从队尾弹出。

3. 为什么队列要严格单调递减?

队列的单调性确保了队首元素始终是当前窗口的最大值。
每次窗口滑动后,我们可以直接从队首获取最大值,时间复杂度为 O(1)。

4. 为什么存储下标而不是元素值?

存储下标可以方便我们判断元素是否在当前窗口内。
如果只存储元素值,当窗口滑动时,我们无法知道该元素是否已经超出窗口边界。

五、复杂度分析

  • 时间复杂度:O(n)。每个元素最多入队和出队一次,因此时间复杂度是线性的。
  • 空间复杂度:O(1)。只需开辟一个新数组和双端队列。

六、总结:单调队列的适用场景

滑动窗口最大值问题是单调队列的经典应用。
当遇到需要在滑动窗口中快速查找最大值或最小值的问题时,都可以考虑使用单调队列来优化。
核心思想:通过维护一个单调队列,避免重复比较,将时间复杂度从 O(nk) 降低到 O(n)。
掌握单调队列的思想后,类似的问题如滑动窗口最小值、区间最大值等都可以迎刃而解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值