力扣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 nums[i] 队列操作 队列状态(存储下标) 对应元素值 是否记录结果
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解法详细解析,重点掌握单调队列这一最优解法,它是解决滑动窗口最值问题的核心技巧。如果觉得文章不错,欢迎点赞、收藏、关注三连支持!

相关推荐
摩尔曼斯克的海2 小时前
力扣面试题--双指针类
python·算法·leetcode
灰色小旋风2 小时前
力扣——第7题(C++)
c++·算法·leetcode
故事和你913 小时前
sdut-程序设计基础Ⅰ-实验二选择结构(1-8)
大数据·开发语言·数据结构·c++·算法·优化·编译原理
努力学算法的蒟蒻3 小时前
day106(3.7)——leetcode面试经典150
算法·leetcode·面试
Σίσυφος19003 小时前
PCL聚类 之 欧式聚类(最常用)
算法·机器学习·聚类
所谓伊人,在水一方3334 小时前
【Python数据科学实战之路】第12章 | 无监督学习算法实战:聚类与降维的奥秘
python·sql·学习·算法·信息可视化·聚类
像素猎人4 小时前
数据结构之顺序表的插入+删除+查找+修改操作【主函数一步一输出,代码更加清晰直观】
数据结构·c++·算法
季明洵4 小时前
二叉树的最小深度、完全二叉树的节点个数、平衡二叉树、路径总和、从中序与后序遍历序列构造二叉树
java·数据结构·算法·leetcode·二叉树
想进个大厂4 小时前
代码随想录day63 64 65 66 图论08 09 10 11
c++·算法·图论