算法题解记录--239滑动窗口最大值

239. 滑动窗口最大值 - 解题记录

📌 题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

示例 1:

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

示例 2:

复制代码
输入:nums = [1], k = 1
输出:[1]

🧠 思路演进过程

思路一:暴力解法(最容易想到)

每次窗口移动后,遍历窗口内的 k 个元素,找出最大值。

java 复制代码
for (int i = 0; i <= nums.length - k; i++) {
    int max = nums[i];
    for (int j = i; j < i + k; j++) {
        max = Math.max(max, nums[j]);
    }
    result[i] = max;
}

时间复杂度: O(n × k)
问题: 数据量大时会超时 ❌


思路二:优化思路(记录最大值)

每次滑动窗口,只引入一个新元素,丢弃一个旧元素:

  • 如果新元素 ≥ 当前最大值 → 新元素成为最大值
  • 如果新元素 < 当前最大值:
    • 如果丢弃的不是当前最大值 → 最大值不变
    • 如果丢弃的是当前最大值 → 重新遍历窗口找最大值

问题: 数组递减时,每次都要重新遍历,复杂度仍为 O(n × k)


思路三:单调队列(最优解)✨

使用双端队列(Deque)维护一个单调递减 的队列,存储的是数组元素的下标。

核心思想:

  • 队首永远是当前窗口的最大值
  • 保持队列中下标对应的元素值从大到小
  • 移除过期元素(不在当前窗口)
  • 移除不可能成为最大值的元素(比新元素小的)

🏗️ 单调队列详解

队列特性

特性 说明
存储内容 数组元素的下标
队首 → 队尾 下标对应元素值单调递减
队首作用 当前窗口最大值的下标
队尾作用 插入新元素的位置

操作规则

  1. 移除过期元素:如果队首下标 ≤ i - k,说明已离开窗口,弹出队首
  2. 保持单调性:从队尾开始,移除所有比新元素小的元素(它们再也没机会成为最大值)
  3. 插入新元素:将新元素下标加入队尾
  4. 收集结果:当 i ≥ k-1 时,队首下标对应的元素就是当前窗口的最大值

🎬 动图演示

以 nums = [1,3,-1,-3,5,3,6,7], k = 3 为例:

步骤 窗口 队列(存下标) 队列对应值 最大值
i=0 1 0 1 -
i=1 1,3 1 3 -
i=2 1,3,-1 1,2 3,-1 3
i=3 3,-1,-3 1,2,3 3,-1,-3 3
i=4 -1,-3,5 4 5 5
i=5 -3,5,3 4,5 5,3 5
i=6 5,3,6 6 6 6
i=7 3,6,7 7 7 7

💻 代码实现

java 复制代码
class Solution {
    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 ArrayDeque<>(); // 存储下标
        
        for (int i = 0; i < n; i++) {
            // 1. 移除不在窗口内的元素(从队首)
            if (!deque.isEmpty() && deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }
            
            // 2. 保持单调递减:移除所有比新元素小的元素(从队尾)
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }
            
            // 3. 加入新元素
            deque.offerLast(i);
            
            // 4. 收集结果(从第 k-1 个元素开始)
            if (i >= k - 1) {
                result[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        
        return result;
    }
}

⚙️ 复杂度分析

复杂度 说明
时间复杂度 O(n) - 每个元素最多入队出队一次
空间复杂度 O(k) - 队列最多存储 k 个元素

📝 关键点总结

  1. 为什么存下标不存值?

    • 需要判断元素是否还在窗口内(通过下标和 i-k 比较)
    • 通过下标可以快速获取元素值
  2. 为什么用双端队列?

    • 需要从队首移除过期元素
    • 需要从队尾移除比新元素小的元素
    • 需要从队尾添加新元素
  3. 单调队列的本质

    • 维护一个"候选最大值"的序列
    • 新元素会"干掉"所有比它小的旧元素
    • 队首永远是当前最强选手

🎯 举一反三

这种单调队列的思想还可以解决:

  • 滑动窗口最小值
  • 滑动窗口最值问题变种
  • 需要维护一个动态集合的最值问题

📚 参考资料

  • Java Deque 接口:ArrayDeque 是常用实现类
  • 关键方法:
    • peekFirst() / peekLast() - 查看队首/队尾
    • pollFirst() / pollLast() - 移除并返回队首/队尾
    • offerFirst() / offerLast() - 添加元素到队首/队尾

💡 记住:单调队列就是用空间换时间,用 O(n) 的代价解决原本 O(n×k) 的问题!

相关推荐
其实防守也摸鱼13 小时前
个人学习 Pwn 思路:从基础到漏洞利用的进阶路径
大数据·前端·人工智能·chrome·算法·安全·网络安全
Nuanyt14 小时前
LeetCode Hot 100 链表专题(五)· 2 题(Java)
java·算法·leetcode·链表
w_zero_one17 小时前
递归算法(一)
java·开发语言·算法
风影同志7832419 小时前
20 个比特能装下多少行情?——实测拆解 Kronos 的 K 线分词器
算法
写的都是BUG19 小时前
APK 里的钥匙,一直就放在门口:聊聊加固真正该做什么
前端·算法
地平线开发者20 小时前
CLIP 的零样本分类:一组文本如何变成图像分类器?
算法
京东云开发者20 小时前
从“会生成”到“能生产”:萤火虫 AIGC 平台的 AI Infra 实践
算法·aigc·产品
youshi666820 小时前
PID——算法改进
算法
一只不想敲代码的程序猿1 天前
如何更高效地拆解问题?
算法