• 优先队列题目:滑动窗口最大值


    题目

    标题和出处

    标题:滑动窗口最大值

    出处:239. 滑动窗口最大值

    难度

    6 级

    题目描述

    要求

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

    返回滑动窗口中的最大值。

    示例

    示例 1:

    输入: nums   =   [1,3,-1,-3,5,3,6,7],   k   =   3 \texttt{nums = [1,3,-1,-3,5,3,6,7], k = 3} nums = [1,3,-1,-3,5,3,6,7], k = 3
    输出: [3,3,5,5,6,7] \texttt{[3,3,5,5,6,7]} [3,3,5,5,6,7]
    解释:

    滑动窗口的位置最大值
    [1    3    -1]   -3    5    3    6    7   \texttt{[1~~3~~-1]~-3~~5~~3~~6~~7~} [1  3  -1] -3  5  3  6  7  3 \texttt{3} 3
      1   [3    -1    -3]   5    3    6    7   \texttt{~1~[3~~-1~~-3]~5~~3~~6~~7~}  1 [3  -1  -3] 5  3  6  7  3 \texttt{3} 3
      1    3   [-1    -3    5]   3    6    7   \texttt{~1~~3~[-1~~-3~~5]~3~~6~~7~}  1  3 [-1  -3  5] 3  6  7  5 \texttt{5} 5
      1    3    -1   [-3    5    3]   6    7   \texttt{~1~~3~~-1~[-3~~5~~3] 6~~7~}  1  3  -1 [-3  5  3] 6  7  5 \texttt{5} 5
      1    3    -1    -3   [5    3    6]   7   \texttt{~1~~3~~-1~~-3~[5~~3~~6]~7~}  1  3  -1  -3 [5  3  6] 7  6 \texttt{6} 6
      1    3    -1    -3    5   [3    6    7] \texttt{~1~~3~~-1~~-3~~5~[3~~6~~7]}  1  3  -1  -3  5 [3  6  7] 7 \texttt{7} 7

    示例 2:

    输入: nums   =   [1],   k   =   1 \texttt{nums = [1], k = 1} nums = [1], k = 1
    输出: [1] \texttt{[1]} [1]

    示例 3:

    输入: nums   =   [1,-1],   k   =   1 \texttt{nums = [1,-1], k = 1} nums = [1,-1], k = 1
    输出: [1,-1] \texttt{[1,-1]} [1,-1]

    示例 4:

    输入: nums   =   [9,11],   k   =   2 \texttt{nums = [9,11], k = 2} nums = [9,11], k = 2
    输出: [11] \texttt{[11]} [11]

    示例 5:

    输入: nums   =   [4,-2],   k   =   2 \texttt{nums = [4,-2], k = 2} nums = [4,-2], k = 2
    输出: [4] \texttt{[4]} [4]

    数据范围

    • 1 ≤ nums.length ≤ 10 5 \texttt{1} \le \texttt{nums.length} \le \texttt{10}^\texttt{5} 1nums.length105
    • -10 4 ≤ nums[i] ≤ 10 4 \texttt{-10}^\texttt{4} \le \texttt{nums[i]} \le \texttt{10}^\texttt{4} -104nums[i]104
    • 1 ≤ k ≤ nums.length \texttt{1} \le \texttt{k} \le \texttt{nums.length} 1knums.length

    前言

    假设数组 nums \textit{nums} nums 的长度是 n n n,则数组 nums \textit{nums} nums 的大小为 k k k 的滑动窗口有 n − k + 1 n - k + 1 nk+1 个。最直观的做法是遍历每个大小为 k k k 的滑动窗口寻找最大值,该做法的时间复杂度是 O ( n k ) O(nk) O(nk),会超出时间限制,因此需要优化。

    有两种优化方法,一是优先队列,二是单调队列。

    解法一

    思路和算法

    对于最大值问题,可以使用基于大顶堆的优先队列,优先队列的队首元素为优先队列中的最大元素。从左到右遍历数组 nums \textit{nums} nums 并依次将每个元素加入优先队列,则当遍历到下标 i i i 时,优先队列的队首元素为数组 nums \textit{nums} nums 从下标 0 0 0 到下标 i i i 的全部元素中的最大元素。

    由于题目要求计算每个滑动窗口的最大值,因此对于每个滑动窗口,优先队列中的最大元素必须位于该滑动窗口的下标范围内,即当遍历到下标 i i i 时,如果 i ≥ k − 1 i \ge k - 1 ik1,则优先队列中的最大元素对应的下标必须在范围 [ i − k + 1 , i ] [i - k + 1, i] [ik+1,i] 内。为了确保优先队列中的最大元素对应的下标在滑动窗口的下标范围内,优先队列需要同时存储元素和对应下标,优先队列仍满足队首元素为优先队列中的最大元素。

    创建长度为 n − k + 1 n - k + 1 nk+1 的数组 maxArray \textit{maxArray} maxArray 作为结果数组,则 maxArray [ i ] \textit{maxArray}[i] maxArray[i] 表示数组 nums \textit{nums} nums 从下标 i i i 到下标 i + k − 1 i + k - 1 i+k1 的滑动窗口中的最大值。

    首先遍历 nums [ 0 ] \textit{nums}[0] nums[0] nums [ k − 1 ] \textit{nums}[k - 1] nums[k1],将每个元素和对应下标加入优先队列,令 maxArray [ 0 ] \textit{maxArray}[0] maxArray[0] 等于这 k k k 个元素中的最大值。然后遍历 nums [ k ] \textit{nums}[k] nums[k] nums [ n − 1 ] \textit{nums}[n - 1] nums[n1],对于每个下标 i i i,其对应的滑动窗口的下标范围是 [ i − k + 1 , i ] [i - k + 1, i] [ik+1,i],进行如下操作:

    1. nums [ i ] \textit{nums}[i] nums[i] 和下标 i i i 加入优先队列;

    2. 如果优先队列的队首元素对应下标小于等于 i − k i - k ik,则将优先队列的队首元素取出,重复该过程直到优先队列的队首元素对应下标大于 i − k i - k ik

    3. 将优先队列的队首元素赋给 maxArray [ i − k + 1 ] \textit{maxArray}[i - k + 1] maxArray[ik+1]

    遍历结束之后, maxArray \textit{maxArray} maxArray 的每个下标处的元素即为以该下标作为开始下标的滑动窗口中的最大值。

    代码

    class Solution {
        public int[] maxSlidingWindow(int[] nums, int k) {
            int length = nums.length;
            int[] maxArray = new int[length - k + 1];
            PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
                public int compare(int[] arr1, int[] arr2) {
                    return arr2[0] - arr1[0];
                }
            });
            for (int i = 0; i < k; i++) {
                pq.offer(new int[]{nums[i], i});
            }
            maxArray[0] = pq.peek()[0];
            for (int i = k; i < length; i++) {
                pq.offer(new int[]{nums[i], i});
                while (pq.peek()[1] <= i - k) {
                    pq.poll();
                }
                maxArray[i - k + 1] = pq.peek()[0];
            }
            return maxArray;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    复杂度分析

    • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。由于优先队列中最多有 n n n 个元素,因此每次将元素加入优先队列和从优先队列中取出的时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn),总时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

    • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。空间复杂度主要取决于优先队列,优先队列中的元素个数不会超过 n n n

    解法二

    思路和算法

    解法一的时间复杂度并非最优,因为每次在优先队列中加入元素和取出元素时都需要维护优先队列的性质。可以使用单调队列降低时间复杂度,单调队列存储数组 nums \textit{nums} nums 的下标,满足从队首到队尾的下标对应的元素单调递减。

    从左到右遍历数组 nums \textit{nums} nums,依次将下标加入单调队列。为了维护队列的单调性,每次将下标加入单调队列之前,需要首先比较队尾下标对应的元素和当前元素,如果队列不为空且队尾下标对应的元素小于等于当前元素,则将队尾下标出队列,重复该操作直到队列为空或者队尾下标对应的元素大于当前元素,此时再将当前下标在队尾入队列。

    由于第一个大小为 k k k 的滑动窗口的下标范围是 [ 0 , k − 1 ] [0, k - 1] [0,k1],因此首先将下标 0 0 0 k − 1 k - 1 k1 依次加入单调队列,在此过程中维护队列的单调性。将下标 0 0 0 k − 1 k - 1 k1 加入单调队列之后,队首下标对应的元素即为第一个大小为 k k k 的滑动窗口的最大值。

    当遍历到的下标 i i i 大于 k − 1 k - 1 k1 时,以下标 i i i 结尾的滑动窗口的开始下标大于 0 0 0,因此单调队列的队首下标可能在当前滑动窗口的下标范围之外,此时需要将队首下标出队列。具体做法是,对于 k ≤ i < n k \le i < n ki<n,当遍历到下标 i i i 时,如果单调队列的队首下标是 i − k i - k ik,则将队首下标出队列,以确保单调队列中的下标都在当前滑动窗口的下标范围内。

    当单调队列中的下标都在当前滑动窗口的下标范围内时,在维护队列的单调性的前提下将当前下标 i i i 在队尾入队列,此时队首下标对应的元素即为当前滑动窗口的最大值。

    代码

    class Solution {
        public int[] maxSlidingWindow(int[] nums, int k) {
            int length = nums.length;
            int[] maxArray = new int[length - k + 1];
            Deque<Integer> queue = new ArrayDeque<Integer>();
            for (int i = 0; i < k; i++) {
                int num = nums[i];
                while (!queue.isEmpty() && nums[queue.peekLast()] < num) {
                    queue.pollLast();
                }
                queue.offerLast(i);
            }
            maxArray[0] = nums[queue.peekFirst()];
            for (int i = k; i < length; i++) {
                int windowIndex = i - k + 1;
                if (!queue.isEmpty() && queue.peekFirst() == i - k) {
                    queue.pollFirst();
                }
                int num = nums[i];
                while (!queue.isEmpty() && nums[queue.peekLast()] < num) {
                    queue.pollLast();
                }
                queue.offerLast(i);
                maxArray[windowIndex] = nums[queue.peekFirst()];
            }
            return maxArray;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    复杂度分析

    • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。需要遍历数组 nums \textit{nums} nums 一次,由于每个下标最多入队列和出队列各一次,因此时间复杂度是 O ( n ) O(n) O(n)

    • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。空间复杂度主要取决于单调队列,单调队列中的元素个数不会超过 n n n

  • 相关阅读:
    计算机编码规则之:Base64编码
    Apache paimon 优化
    COCO格式json切分为labelme可识别json
    $set小概率的赋值失败问题都被我碰上了
    毕业设计-基于机器视觉的行人车辆跟踪出入双向检测计数
    人工智能基础第三次作业
    排序方法总结
    架构师:构建高可用服务治理Consul集群与Kong网关管理
    简单介绍一下 git reflog
    通俗易懂,多位P8以及以上的大佬联袂编写设计模式核
  • 原文地址:https://blog.csdn.net/stormsunshine/article/details/121237260