239. 滑动窗口最大值(Sliding Window Maximum)

频次 ★★★★★ · 难度 🔴 · 高频:字节/阿里/美团

题目

长度 k 的窗口从左到右滑过数组,输出每个位置的窗口最大值。

示例

输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]

思路

单调递减双端队列(存下标):

  • 队头永远是当前窗口最大值的下标
  • 新元素入队前,从队尾弹掉所有比它小的——它们在新元素在场期间永无出头之日
  • 队头下标滑出窗口范围(<= i - k)时从队头弹出

每个下标进出队各一次,O(n)。

代码

public int[] maxSlidingWindow(int[] nums, int k) {
    Deque<Integer> dq = new ArrayDeque<>();   // 存下标,对应值单调递减
    int n = nums.length;
    int[] res = new int[n - k + 1];
    for (int i = 0; i < n; i++) {
        while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i])
            dq.pollLast();                     // 弹掉"更小且更旧"的
        dq.offerLast(i);
        if (dq.peekFirst() <= i - k) dq.pollFirst();  // 队头滑出窗口
        if (i >= k - 1) res[i - k + 1] = nums[dq.peekFirst()];
    }
    return res;
}

复杂度

  • 时间:O(n) —— 每个元素至多进队/出队一次
  • 空间:O(k) —— 队列长度不超过窗口

边界条件

  • k = 1:输出原数组
  • k = n:只输出全局最大值
  • 全递减数组:队列长度达到 k,队头持续输出

变式

  • 滑动窗口最小值:队列改单调递增
  • 队列存值而非下标:无法判断队头是否过期,必须存下标——面试常见的追问点
  • 求窗口最大值之和/差:同款队列,只改输出

易错点

  • 弹队尾的条件 <=(相等也弹):留新不留旧,旧的先过期
  • 判断过期用下标dq.peekFirst() <= i - k),不能用值——窗口里可能有重复值
  • 出结果的时机是 i >= k - 1,结果下标 i - k + 1
  • LinkedList 也能当 Deque,但 ArrayDeque 无结点分配开销更快,见集合框架

面试追问

  • 为什么不用大顶堆? 堆能做但 O(n log n):过期元素懒删除,堆顶出窗时才弹。单调队列把”过期”和”被压制”两类无用元素都及时清掉才到 O(n)——能对比两种方案取舍再给结论
  • 单调队列和单调栈什么关系? 都维护单调性淘汰”不可能成为答案”的候选;栈只关心一端(最近的更大/更小),队列因为窗口会过期需要两端操作

关联题