【LeetCode 热题 100】239. 滑动窗口最大值——(解法三)滑动窗口+单调队列+数组

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

【LeetCode 热题 100】239. 滑动窗口最大值——(解法一)滑动窗口+暴力解
【LeetCode 热题 100】239. 滑动窗口最大值——(解法二)滑动窗口+单调队列

整体思路

这段代码同样是解决 “滑动窗口最大值” 问题的最优解法,其核心算法依然是 单调队列。但与使用Java标准库 ArrayDeque 的版本不同,此实现通过一个普通数组 q 和两个整型指针 headtail 来手动模拟一个双端队列。这种方式在性能上可能略有优势,因为它避免了库函数的调用开销和潜在的自动装箱/拆箱。

算法的逻辑与上一个版本完全一致,只是数据结构的手动实现带来了语法上的差异:

  1. 数据结构模拟

    • int[] q = new int[n]: 创建一个足够大的数组来充当队列的底层存储。存储的仍然是 nums 数组的索引
    • int head = 0: 模拟队首指针。q[head] 是队首元素。
    • int tail = -1: 模拟队尾指针。q[tail] 是队尾元素。tail < head 的状态表示队列为空。
  2. 单调队列的维护

    • 算法通过一个 right 指针遍历 nums 数组,对每个新元素进行处理。
      a. 维护单调性 (队尾操作)
      • while (head <= tail && nums[q[tail]] <= nums[right]): 在添加新元素 nums[right] 的索引前,检查队尾。如果队尾索引对应的元素小于等于新元素,说明队尾元素已无用,将其“弹出”。
      • tail--: 这是手动的“队尾弹出”操作,直接移动 tail 指针即可。
        b. 入队 (队尾操作)
      • q[++tail] = right;: 手动的“队尾压入”操作。先将 tail 指针加一,然后在新的 tail 位置存入 right 索引。
        c. 维护窗口大小 (队首操作)
      • if (right - q[head] + 1 > k): 检查队首索引 q[head] 是否已滑出当前窗口。
      • head++: 手动的“队首弹出”操作,移动 head 指针,抛弃过期的元素。
        d. 记录结果
      • if (right >= k - 1): 当窗口形成后,队首 q[head] 存储的索引始终对应着当前窗口的最大值。
      • ans[right - k + 1] = nums[q[head]]: 记录结果。

通过手动管理数组和指针,该实现以最底层、最高效的方式实现了单调队列算法。

完整代码

class Solution {
    /**
     * 计算滑动窗口的最大值(优化版,手动模拟队列)。
     * @param nums 整数数组
     * @param k 窗口大小
     * @return 包含每个窗口最大值的数组
     */
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        // 计算结果数组的长度
        int m = n - k + 1;
        int[] ans = new int[m];
        
        // 使用普通数组 q 来模拟一个双端队列,存储元素的索引
        int[] q = new int[n];
        // head 是队首指针,指向 q 数组中的索引
        int head = 0;
        // tail 是队尾指针,指向 q 数组中的索引。tail < head 表示队列为空
        int tail = -1;
        
        // right 指针作为窗口的右边界,遍历整个数组
        for (int right = 0; right < n; right++) {
            // 步骤 1: 维护队列的单调递减性
            // 当队列不为空 (head <= tail),且队尾元素小于等于当前元素时,
            // 将队尾元素“弹出”。
            while (head <= tail && nums[q[tail]] <= nums[right]) {
                // 通过移动 tail 指针实现“队尾弹出”
                tail--;
            }
            // 将当前元素的索引“压入”队尾
            q[++tail] = right;
            
            // 步骤 2: 维护窗口的有效范围
            // 检查队首元素的索引是否已经滑出窗口左边界
            if (right - q[head] + 1 > k) {
                // 通过移动 head 指针实现“队首弹出”
                head++;
            }
            
            // 步骤 3: 记录结果
            // 当窗口大小达到 k 时,开始记录最大值
            if (right >= k - 1) {
                // 队首 q[head] 始终是当前窗口最大值的索引
                ans[right - k + 1] = nums[q[head]];
            }
        }
        return ans;
    }
}

时空复杂度

时间复杂度:O(N)

  1. 循环:外层 for 循环遍历 nums 数组一次,执行 N 次。
  2. 指针操作
    • right 指针从 0 移动到 N-1
    • tail 指针在 [-1, N-1] 的范围内移动。每个元素的索引 right 最多使 tail 增加一次。
    • head 指针在 [0, N] 的范围内移动。
    • 关键在于,nums 中的每个元素的索引最多只会被压入 q 数组一次(通过 tail++),也最多只会被移出一次(通过 head++tail--)。
    • 因此,所有对 headtail 指针的操作,在整个算法生命周期内的总次数是 O(N) 级别。
    • 将这些操作的成本均摊N 次外层循环中,每次循环的均摊时间复杂度为 O(1)

综合分析
算法由 N 次均摊时间复杂度为 O(1) 的操作构成,因此总的时间复杂度是 O(N)

空间复杂度:O(N)

  1. 主要存储开销:算法使用了一个整型数组 q 来模拟队列。
  2. 空间大小
    • 代码中 q 被初始化为 new int[n],直接分配了与输入数组等大的空间。因此,从内存分配的角度看,空间复杂度是 O(N)
    • 值得注意的细节:虽然分配了 O(N) 的空间,但队列在任意时刻的实际元素数量(即 tail - head + 1)并不会超过窗口大小 k。所以,算法在逻辑上使用的空间是 O(k)。但在分析代码的实际内存占用时,应以上限 O(N) 为准。
  3. 结果数组ans 数组不计入额外辅助空间。

综合分析
根据代码 int[] q = new int[n]; 的直接内存分配,该算法的额外辅助空间复杂度为 O(N)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xumistore

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值