Java详解LeetCode 热题 100(11):LeetCode 239. 滑动窗口最大值(Sliding Window Maximum)详解

文章目录

    • 1. 题目描述
    • 2. 理解题目
    • 3. 解法一:暴力法
      • 3.1 思路
      • 3.2 Java代码实现
      • 3.3 代码详解
      • 3.4 复杂度分析
      • 3.5 适用场景
    • 4. 解法二:优先队列(最大堆)
      • 4.1 思路
      • 4.2 Java代码实现
      • 4.3 代码详解
      • 4.4 复杂度分析
      • 4.5 适用场景
    • 5. 解法三:双端队列(Deque)
      • 5.1 思路
      • 5.2 Java代码实现
      • 5.3 代码详解
      • 5.4 复杂度分析
      • 5.5 适用场景
    • 6. 解法四:动态规划法
      • 6.1 思路
      • 6.2 Java代码实现
      • 6.3 代码详解
      • 6.4 复杂度分析
      • 6.5 适用场景
    • 7. 优化与提升
      • 7.1 滑动窗口处理技巧
      • 7.2 双端队列优化
      • 7.3 处理大规模数据
    • 8. 进阶思考与变体问题
      • 8.1 滑动窗口最小值
      • 8.2 多个窗口大小
      • 8.3 实时数据流中的滑动窗口最大值
    • 9. 常见错误与优化
      • 9.1 常见错误
      • 9.2 性能优化
    • 10. 完整的 Java 解决方案
    • 11. 实际运用示例
      • 11.1 LeetCode提交结果
      • 11.2 应用场景
      • 11.3 扩展用例
    • 12. 总结与技巧
      • 12.1 解题要点
      • 12.2 学习收获
      • 12.3 面试技巧
    • 13. 参考资料

1. 题目描述

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

示例 3:

输入:nums = [1,-1], k = 1
输出:[1,-1]

示例 4:

输入:nums = [9,11], k = 2
输出:[11]

示例 5:

输入:nums = [4,-2], k = 2
输出:[4]

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

2. 理解题目

这道题需要我们求解滑动窗口内的最大值。滑动窗口是固定大小为k的子数组,从数组左端开始,每次向右移动一个位置,直到窗口右端达到数组末尾。

关键理解:

  1. 窗口大小固定为k
  2. 窗口每次向右移动一个位置
  3. 需要返回每个窗口位置的最大值
  4. 最终结果是一个长度为n-k+1的数组,其中n是原数组长度

例如,对于示例1中的数组[1,3,-1,-3,5,3,6,7]和k=3:

  • 第一个窗口包含元素[1,3,-1],最大值是3
  • 第二个窗口包含元素[3,-1,-3],最大值是3
  • 第三个窗口包含元素[-1,-3,5],最大值是5
  • 依此类推…

最终输出:[3,3,5,5,6,7]

3. 解法一:暴力法

3.1 思路

最直接的思路是对每个窗口位置,遍历窗口内的所有元素来找出最大值。

具体步骤:

  1. 对于每个窗口位置(从0到n-k),遍历窗口内的k个元素
  2. 找出这k个元素中的最大值
  3. 将最大值添加到结果数组中

3.2 Java代码实现

public class Solution {
   
    public int[] maxSlidingWindow(int[] nums, int k) {
   
        int n = nums.length;
        if (n == 0 || k == 0) {
   
            return new int[0];
        }
        
        // 结果数组长度为 n-k+1
        int[] result = new int[n - k + 1];
        
        // 对每个窗口位置
        for (int i = 0; i <= n - k; i++) {
   
            int max = nums[i]; // 初始化为窗口内第一个元素
            
            // 遍历窗口内的其他元素
            for (int j = i + 1; j < i + k; j++) {
   
                max = Math.max(max, nums[j]);
            }
            
            result[i] = max; // 保存当前窗口的最大值
        }
        
        return result;
    }
}

3.3 代码详解

  1. 首先处理边界情况:如果数组为空或窗口大小为0,返回空数组
  2. 创建结果数组,其长度为n-k+1
  3. 外层循环遍历每个窗口的起始位置
  4. 内层循环遍历当前窗口内的元素,找出最大值
  5. 将当前窗口的最大值存入结果数组

3.4 复杂度分析

  • 时间复杂度:O(n*k),其中n是数组长度。对于每个窗口位置(共n-k+1个),我们需要O(k)时间找出窗口内的最大值。
  • 空间复杂度:O(n-k+1),即结果数组的大小。

3.5 适用场景

暴力法适用于数组长度不大且窗口大小较小的情况。当数组长度或窗口大小较大时,这种方法效率较低。

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

4.1 思路

我们可以使用优先队列(最大堆)来维护窗口中的元素,这样可以在O(log k)时间内获取窗口中的最大值。

为了处理元素移出窗口的情况,我们需要在队列中存储元素值和索引的对,这样可以判断堆顶元素是否还在当前窗口内。

具体步骤:

  1. 创建一个最大堆,存储元素值和索引的对
  2. 初始将前k个元素加入堆
  3. 对于每个窗口位置:
    • 检查堆顶元素是否在当前窗口内(通过索引判断)
    • 如果不在,弹出堆顶元素,直到找到一个在窗口内的元素
    • 将当前窗口的最大值(堆顶元素)添加到结果中
    • 将下一个元素加入堆(如果存在)

4.2 Java代码实现

import java.util.PriorityQueue;
import java.util.Comparator;

public class Solution {
   
    public int[] maxSlidingWindow(int[] nums, int k) {
   
        int n = nums.length;
        if (n == 0 || k == 0) {
   
            return new int[0];
        }
        
        // 结果数组
        int[] result = new int[n - k + 1];
        
        // 创建最大堆,按元素值降序排列
        PriorityQueue<int[]> maxHeap = new PriorityQueue<>(
            (a, b) -> b[0] - a[0] // 降序排列,最大值在堆顶
        );
        
        // 初始将前k个元素加入堆
        for (int i = 0; i < k; i++) {
   
            maxHeap.offer(new int[]{
   nums[i], i});
        }
        
        // 获取第一个窗口的最大值
        result[0] = maxHeap.peek()[0];
        
        // 处理后续窗口
        for (int i = k; i < n; i++) {
   
            // 添加当前元素到堆
            maxHeap.offer(new int[]{
   nums[i], i});
            
            // 移除不在当前窗口内的元素
            while (!maxHeap.isEmpty() && maxHeap.peek()[1] <= i - k) {
   
                maxHeap.poll();
            }
            
            // 当前窗口的最大值
            result[i - k + 1] = maxHeap.peek()[0];
        }
        
        return result;
    }
}

4.3 代码详解

  1. 处理边界情况:如果数组为空或窗口大小为0,返回空数组
  2. 创建结果数组,其长度为n-k+1
  3. 创建最大堆,存储元素值和索引的二元组,按元素值降序排列
  4. 初始将前k个元素加入堆,并获取第一个窗口的最大值
  5. 对于每个后续窗口:
    • 将窗口中的新元素加入堆
    • 移除不在当前窗口内的元素(即索引小于或等于i-k的元素)
    • 将堆顶元素的值(当前窗口最大值)存入结果数组

4.4 复杂度分析

  • 时间复杂度:O(n log k),其中n是数组长度,k是窗口大小。每个元素最多入堆和出堆一次,每次操作时间为O(log k)。
  • 空间复杂度:O(k),优先队列中最多有k个元素。

4.5 适用场景

优先队列法适用于窗口大小较大的情况,因为它对每个窗口的处理不需要遍历所有k个元素。但当k很小时,暴力法可能更快。

5. 解法三:双端队列(Deque)

5.1 思路

双端队列法是本题的最优解法之一。它通过维护一个单调递减的队列(存储元素索引),保证队首始终是当前窗口的最大元素索引。

关键思想:

  • 如果一个元素比它前面的元素大,那么前面的元素就永远不会成为窗口的最大值
  • 保持队列中的元素索引对应的值是单调递减的
  • 队首元素始终是当前窗口的最大值索引

具体步骤:

  1. 创建一个双端队列,存储元素索引
  2. 遍历数组:
    • 移除队列中不在当前窗口的元素(即索引小于i-k+1的元素)
    • 从队尾开始,移除所有小于当前元素的索引
    • 将当前元素的索引加入队尾
    • 如果当前位置大于等于k-1(即已形成第一个窗口),将队首元素对应的值添加到结果中

5.2 Java代码实现

import java.util.ArrayDeque;
import java.util.Deque;

public class Solution {
   
    public int[] maxSlidingWindow(int[] nums, int k) {
   
        int n = nums.length;
        if (n == 0 || k == 0) {
   

你可能感兴趣的:(leetcode,java,算法)