力扣239.滑动窗口的最大值(单调队列维护当前窗口的最值)

33 阅读4分钟

image.png

一、问题回顾

题目:给定数组 nums = [1,3,-1,-3,5,3,6,7] 和窗口大小 k = 3,返回每个滑动窗口的最大值。

窗口位置                最大值
[1  3  -1] -3  5  3  6  73
 1 [3  -1  -3] 5  3  6  73
 1  3 [-1  -3  5] 3  6  75
 1  3  -1 [-3  5  3] 6  75
 1  3  -1  -3 [5  3  6] 76
 1  3  -1  -3  5 [3  6  7]7

结果:[3,3,5,5,6,7]

二、核心思想:为什么用单调队列?

滑动窗口有两个核心操作:

  1. 窗口右移:新元素进入
  2. 窗口左移:旧元素离开

这天然的 "先进先出" 特性,提示我们应该用队列而非栈来维护。而为了快速获取最大值,我们需要队列保持单调递减——队头始终最大。

关键洞察 :当一个新元素进入窗口时,它比队列中所有比它小的元素都"更有前途"——这些小的元素再也不可能成为最大值了(因为它们会先被滑出窗口)。因此,我们可以放心地把它们移除。


三、三大黄金法则

维护单调队列只需记住三条规则:

法则1:队尾入队,保持递减

// 从队尾开始,移除所有小于新元素的值
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
    deque.pollLast();
}
deque.offerLast(i);  // 新元素从队尾入队

效果:队列从头到尾永远是递减的,如 [5, 3, 1]

法则2:队头过期,及时清理

// 如果队头元素已滑出窗口(下标超出范围)
if (!deque.isEmpty() && deque.peekFirst() <= i - k) {
    deque.pollFirst();
}

效果:确保队头元素一定在当前窗口内

法则3:队头即答案

// 当前窗口的最大值就是队头元素
max = nums[deque.peekFirst()];

四、图解全过程

让我们用 nums = [1,3,-1,-3,5,3,6,7], k = 3 实战演练:

步骤i当前窗口操作说明队列(下标)队列值最大值
10[1]1入队[0][1]-
21[1,3]3比1大,1永无出头之日,移除1,3入队[1][3]-
32[1,3,-1]-1比3小,直接入队[1,2][3,-1]3
43[3,-1,-3]-3入队[1,2,3][3,-1,-3]3
54[-1,-3,5]5王者降临,比-1、-3都大,全部移除,5入队[4][5]5
65[-3,5,3]3比5小,入队[4,5][5,3]5
76[5,3,6]6出现,3出局,6比5小?不,6>5,5也出局![6][6]6
87[3,6,7]7王者降临,6移除,7入队[7][7]7

动画演示

窗口滑动 → [1 3 -1][3 -1 -3][-1 -3 5] → ...
            ↓           ↓           ↓
队列变化    [1][3]    [3,-1][3,-1,-3]  [5]  ...
            ↑           ↑           ↑
          13干掉    -3加入      -1,-35干掉

五、完整代码实现

Java 版本(推荐)

public int[] maxSlidingWindow(int[] nums, int k) {
    if (nums == null || nums.length == 0) return new int[0];
    
    int n = nums.length;
    int[] result = new int[n - k + 1];
    Deque<Integer> deque = new LinkedList<>(); // 存储下标
    
    for (int i = 0; i < n; i++) {
        // 1. 移除队头过期元素(如果窗口已滑过)
        if (!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) {
            result[i - k + 1] = nums[deque.peekFirst()];
        }
    }
    
    return result;
}

六、复杂度分析

  • 时间复杂度:O(n)
    每个元素最多入队一次、出队一次,所有操作都是O(1)

  • 空间复杂度:O(k)
    队列最多存储k个元素下标

对比暴力解法的 O(n*k),这是质的飞跃!


七、总结与升华

单调队列的本质:不是存储所有元素,而是只存储"有潜力成为最大值"的候选者。那些小的、老的元素被无情淘汰,留下的是精简而有效的信息。

这个思想可以迁移到很多场景:

  • 滑动窗口中位数
  • 滑动窗口最大值/最小值
  • 一些动态规划问题

记住三个操作口诀:队尾入队保递减,队头过期要清理,队头永远是答案


八、互动思考

如果题目改为滑动窗口最小值,应该怎么改代码?
(提示:把队列改成单调递增即可)

希望这篇博客能帮你彻底掌握这个经典算法!如果对你有帮助,欢迎点赞收藏~