力扣239. 滑动窗口最大值(Java解法)

题目描述

给你一个整数数组 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]

提示:

  • 1 <= nums.length <= 10^5

  • -10^4 <= nums[i] <= 10^4

  • 1 <= k <= nums.length

解题思路

这道题是滑动窗口类问题的经典题目,需要高效地找出每个窗口的最大值。下面介绍三种解法,从暴力到最优。

解法一:暴力法(不推荐)

最直观的方法是遍历每个窗口,在每个窗口内找出最大值。

算法步骤:

  1. 窗口从0开始滑动到n-k

  2. 对每个窗口,遍历窗口内所有元素找最大值

  3. 将最大值存入结果数组

java 复制代码
public int[] maxSlidingWindow(int[] nums, int k) {
    int n = nums.length;
    int[] result = new int[n - k + 1];
    
    for (int i = 0; i <= n - k; i++) {
        int max = Integer.MIN_VALUE;
        for (int j = i; j < i + k; j++) {
            max = Math.max(max, nums[j]);
        }
        result[i] = max;
    }
    
    return result;
}

复杂度分析:

  • 时间复杂度:O(n·k),每个窗口需要O(k)时间找最大值,n个窗口

  • 空间复杂度:O(1),不考虑结果数组

缺点: 当n和k都很大时(如10^5),会超时-6-7

解法二:优先队列(最大堆)

利用优先队列(最大堆)来维护窗口内的元素,堆顶就是最大值。

算法步骤:

  1. 创建一个最大堆,存储[元素值, 下标]的数组

  2. 先将前k个元素入堆

  3. 记录堆顶元素作为第一个窗口的最大值

  4. 遍历剩余元素:

    • 将新元素入堆

    • 检查堆顶元素的下标是否在当前窗口内,如果不在则弹出

    • 记录当前堆顶元素作为当前窗口的最大值

java 复制代码
import java.util.PriorityQueue;
import java.util.Comparator;

public int[] maxSlidingWindow(int[] nums, int k) {
    int n = nums.length;
    // 最大堆,存储 [值, 下标],按值降序,值相同按下标降序
    PriorityQueue<int[]> pq = new PriorityQueue<>(new Comparator<int[]>() {
        public int compare(int[] a, int[] b) {
            return a[0] != b[0] ? b[0] - a[0] : b[1] - a[1];
        }
    });
    
    // 前k个元素入堆
    for (int i = 0; i < k; i++) {
        pq.offer(new int[]{nums[i], i});
    }
    
    int[] result = new int[n - k + 1];
    result[0] = pq.peek()[0];
    
    // 处理剩余元素
    for (int i = k; i < n; i++) {
        pq.offer(new int[]{nums[i], i});
        // 移除不在窗口内的堆顶元素
        while (pq.peek()[1] <= i - k) {
            pq.poll();
        }
        result[i - k + 1] = pq.peek()[0];
    }
    
    return result;
}

复杂度分析:

  • 时间复杂度:O(n log n),堆的插入和删除操作都是O(log n)

  • 空间复杂度:O(n),堆中最多存储n个元素-1-7

解法三:单调队列(最优解)

使用双端队列(Deque)维护一个单调递减的队列,队列中存储的是元素的下标,且这些下标对应的元素值是从队首到队尾递减的。这样队首始终是当前窗口的最大值-3-6-10

核心思想:

  • 如果新元素比队列尾部元素大,说明尾部元素永远不可能成为最大值(因为新元素更大且更靠后),直接弹出尾部元素

  • 如果队首元素的下标已经滑出窗口,则弹出队首元素

  • 这样队首元素就是当前窗口的最大值

算法步骤:

  1. 创建双端队列deque,用于存储下标

  2. 遍历数组每个元素nums[i]

    • 维护单调性 :当队列不为空且nums[队尾] < nums[i]时,弹出队尾元素(因为当前元素更大,之前的较小元素不再可能成为最大值)

    • 加入当前元素 :将当前下标i加入队尾

    • 移除过期元素 :如果队首下标<= i - k,说明队首元素已经滑出窗口,弹出队首

    • 记录结果 :当i >= k-1时,窗口已形成,队首元素就是当前窗口的最大值

java 复制代码
import java.util.Deque;
import java.util.ArrayDeque;

public int[] maxSlidingWindow(int[] nums, int k) {
    if (nums == null || nums.length == 0 || k <= 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. 维护单调递减:移除所有比当前元素小的队尾元素
        while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
            deque.pollLast();
        }
        
        // 2. 将当前下标加入队尾
        deque.offerLast(i);
        
        // 3. 移除已经滑出窗口的队首元素
        if (deque.peekFirst() <= i - k) {
            deque.pollFirst();
        }
        
        // 4. 当窗口形成时(i >= k-1),记录最大值
        if (i >= k - 1) {
            result[i - k + 1] = nums[deque.peekFirst()];
        }
    }
    
    return result;
}

举例说明:

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

i numsi 队列操作 队列状态(存储下标) 对应元素值 是否记录结果
0 1 加入0 0 1
1 3 3>1,弹出0,加入1 1 3
2 -1 -1<3,加入2 1,2 3,-1 是 → 3
3 -3 -3<-1,加入3 1,2,3 3,-1,-3 是 → 3
4 5 5> -3, -1, 3,依次弹出3,2,1,加入4 4 5 是 → 5
5 3 3<5,加入5 4,5 5,3 是 → 5
6 6 6>3,5,弹出5,4,加入6 6 6 是 → 6
7 7 7>6,弹出6,加入7 7 7 是 → 7

最终结果:3,3,5,5,6,7-3-6

复杂度分析:

  • 时间复杂度:O(n),每个元素最多入队一次、出队一次

  • 空间复杂度:O(k),队列中最多存储k个元素-6-8-10

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
暴力法 O(n·k) O(1) 思路简单 大数据量超时
优先队列 O(n log n) O(n) 利用堆特性 不是最优
单调队列 O(n) O(k) 线性时间,最优解 需要理解单调队列思想

单调队列的深入理解

什么是单调队列?

单调队列是一种特殊的队列,队列中的元素保持单调递增或单调递减。本题中我们使用的是单调递减队列 ,即队首到队尾的元素值是递减的-3-6

为什么使用双端队列?

因为我们需要:

  1. 从队尾弹出较小的元素(维护单调性)

  2. 从队首弹出过期的元素(窗口滑动)

  3. 从队尾加入新元素

这些操作恰好是双端队列(Deque)的特长-3-10

核心操作详解

java 复制代码
// 1. 维护单调性:新元素比队尾元素大,队尾元素永远不可能成为最大值,弹出
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
    deque.pollLast();
}

// 2. 加入新元素
deque.offerLast(i);

// 3. 移除过期元素:队首元素下标已经滑出窗口
if (deque.peekFirst() <= i - k) {
    deque.pollFirst();
}

// 4. 获取当前窗口最大值
int max = nums[deque.peekFirst()];

边界条件和注意事项

  1. 空数组或k=0 :直接返回空数组-10

  2. k=1:每个窗口只有一个元素,结果就是原数组

  3. k >= 数组长度:整个数组就是一个窗口,返回全局最大值

  4. 队列存储的是下标而非值:这样可以方便判断元素是否还在窗口内

  5. Java中Deque的实现 :常用ArrayDequeLinkedListArrayDeque性能更好-5-8

相关题目推荐

  • 力扣59. 滑动窗口的最大值(剑指Offer)

  • 力扣155. 最小栈(单调栈思想)

  • 力扣739. 每日温度(单调栈)

  • 力扣496. 下一个更大元素 I


以上就是力扣239题"滑动窗口最大值"的Java解法详细解析,重点掌握单调队列这一最优解法,它是解决滑动窗口最值问题的核心技巧。如果觉得文章不错,欢迎点赞、收藏、关注三连支持!

相关推荐
江畔柳前堤7 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore9 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考10 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX11 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面11 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号12 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽12 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI12 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者14 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者14 小时前
【征程6】校准量化中HistogramObserver解析
算法