(LeetCode-Hot100)239. 滑动窗口最大值

❌|✅|💡|📌 239. 滑动窗口最大值

🔗 问题简介

LeetCode 239. 滑动窗口最大值

复制代码
题解github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions

📝 题目描述

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

返回 滑动窗口中的最大值


🧪 示例说明

示例 1:

复制代码
输入: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
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

示例 2:

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

💡 解题思路

方法一:暴力法(❌ 超时)

  • 对每个窗口遍历 k 个元素找最大值。
  • 时间复杂度高,不适用于大输入。

方法二:优先队列(堆)(✅ 可行但非最优)

  • 使用最大堆存储 (值, 索引)
  • 每次取堆顶,若索引不在当前窗口内则弹出。
  • 时间复杂度:O(n log n),空间 O(n)。

方法三:单调双端队列(✅ 最优解)

核心思想 :维护一个单调递减的双端队列(deque),队首始终是当前窗口的最大值。

步骤详解:
  1. 初始化 :创建一个双端队列 deque 存储数组索引,结果列表 res
  2. 遍历数组 (i 从 0 到 n-1):
    • 移除队尾较小元素 :若 nums[i] >= nums[deque.back()],则不断弹出队尾,保持队列单调递减。
    • 加入当前索引 :将 i 加入队尾。
    • 移除队首过期索引 :若队首索引 <= i - k(即不在当前窗口),则弹出队首。
    • 记录结果 :当 i >= k - 1(即第一个完整窗口形成),将 nums[deque.front()] 加入结果。
  3. 返回结果数组。

为什么有效?

  • 队列中索引对应的值单调递减 → 队首是最大值。
  • 及时移除过期索引 → 保证队首在窗口内。
  • 每个元素最多入队出队一次 → 线性时间。

💻 代码实现

java:Java 复制代码
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++) {
            // 移除队尾:保持单调递减
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
                deque.pollLast();
            }
            deque.offerLast(i);
            
            // 移除队首过期索引
            if (deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }
            
            // 记录结果(从第k个元素开始)
            if (i >= k - 1) {
                res[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        return res;
    }
}
go:Go 复制代码
func maxSlidingWindow(nums []int, k int) []int {
    if len(nums) == 0 || k <= 0 {
        return []int{}
    }
    n := len(nums)
    res := make([]int, 0, n-k+1)
    deque := make([]int, 0) // 存储索引
    
    for i := 0; i < n; i++ {
        // 移除队尾:保持单调递减
        for len(deque) > 0 && nums[i] >= nums[deque[len(deque)-1]] {
            deque = deque[:len(deque)-1]
        }
        deque = append(deque, i)
        
        // 移除队首过期索引
        if deque[0] <= i-k {
            deque = deque[1:]
        }
        
        // 记录结果(从第k个元素开始)
        if i >= k-1 {
            res = append(res, nums[deque[0]])
        }
    }
    return res
}

🎯 示例演示

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

i numsi deque (索引) deque (值) 是否记录 res
0 1 0 1 \[\]
1 3 1 3 \[\]
2 -1 1,2 3,-1 3
3 -3 1,2,3 3,-1,-3 3,3
4 5 4 5 3,3,5
5 3 4,5 5,3 3,3,5,5
6 6 6 6 3,3,5,5,6
7 7 7 7 3,3,5,5,6,7

✅ 队列始终保持单调递减,队首为窗口最大值。


✅ 答案有效性证明

  1. 正确性

    • 队列单调递减 ⇒ 队首是当前窗口最大值。
    • 过期索引被及时移除 ⇒ 队首始终在窗口 [i-k+1, i] 内。
    • 每个窗口恰好记录一次 ⇒ 结果长度为 n - k + 1
  2. 边界处理

    • k = 1:每个元素单独成窗,结果即原数组。
    • k = n:整个数组一个窗口,结果为全局最大值。

📊 复杂度分析

方法 时间复杂度 空间复杂度 说明
暴力法 O(nk) O(1) 每个窗口遍历k次
优先队列 O(n log n) O(n) 堆操作log n
单调队列 O(n) O(k) 每个元素入队出队各一次

✅ 单调队列是最优解,线性时间,常数空间(忽略结果数组)。


📌 问题总结

  • 关键洞察 :滑动窗口最大值问题本质是动态维护区间最值 ,适合用单调队列
  • 技巧 :队列存索引而非值,便于判断是否过期。
  • 扩展:类似问题如"滑动窗口中位数"可用双堆,"最小值"只需改为单调递增队列。
  • 面试重点:单调队列是高频考点,需熟练掌握其维护逻辑与应用场景。
相关推荐
不正经学生17 分钟前
C语言预处理详解:编译器真正动手之前的那些事
c语言·开发语言·算法·面试·bug
毕竟是shy哥3 小时前
计算YOLO数据集中每个类的目标数
算法·yolo·机器学习
M78佐菲3 小时前
Linux学习笔记:TCP协议
linux·笔记·学习·tcp/ip·算法
学长毕业设计3 小时前
基于SpringBoot的公益基金管理系统(源码+文档+讲解视频)
java·spring boot·后端
东小西3 小时前
【SAA实战】第 3 篇 · 工具调用全攻略:把业务能力交给 Agent 自己调度
java·后端·spring
东小西3 小时前
【SAA实战】第 4 篇 · Agent 短期记忆:saver 让 Agent 跨轮记得住(threadId 隔离)
java·后端·spring
圣保罗的大教堂3 小时前
leetcode 1406. 石子游戏 III 困难
leetcode
许彰午3 小时前
22-DataCenter报文序列化
java·低代码·架构·状态模式
2601_962065254 小时前
[MySQL] SQL优化之性能分析
java·sql·mysql
小范同学_4 小时前
JDK1.7 与 JDK1.8 HashMap 底层原理对比 + 数组并发扩容死循环详解
java·开发语言