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

题目

标题和出处

标题:滑动窗口最大值

出处: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;
    }
}

复杂度分析

  • 时间复杂度: 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;
    }
}

复杂度分析

  • 时间复杂度: 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
这是一个典型的滑动窗口问题。我们可以使用双端队列来解决这个问题。 算法如下: 1. 初始化一个双端队列dq用于存储每个滑动窗口的元素下标。 2. 遍历数组,对于每个元素arr[i],执行以下步骤: a. 如果dq不为空且dq的第一个元素不在当前滑动窗口中(即dq的第一个元素小于i-k+1),则将dq的第一个元素移出队列。 b. 当dq不为空且arr[i]大于等于数组中下标为dq的最后一个元素对应的时,将dq的最后一个元素移出队列。 c. 将i加入dq的末尾。 d. 如果滑动窗口的大小大于等于k,则将当前滑动窗口的最小(即数组中下标为dq的第一个元素对应的)加入结果列表。 e. 如果滑动窗口的大小大于等于k,则将当前滑动窗口的最大(即数组中下标为dq的最后一个元素对应的)加入结果列表。 3. 返回结果列表作为输出。 下面是使用Python实现的代码示例: ```python from collections import deque def sliding_window(nums, k): n = len(nums) dq = deque() res_min = [] res_max = [] for i in range(n): # 移除滑动窗口最左边的元素 if dq and dq[0] < i - k + 1: dq.popleft() # 移除队列中小于等于当前元素的元素 while dq and nums[i] >= nums[dq[-1]]: dq.pop() # 将当前元素加入队列 dq.append(i) # 记录滑动窗口的最小和最大 if i >= k - 1: res_min.append(nums[dq[0]]) res_max.append(nums[dq[-1]]) return res_min, res_max # 读取输入 n, k = map(int, input().split()) nums = list(map(int, input().split())) # 调用函数并输出结果 res_min, res_max = sliding_window(nums, k) print(' '.join(map(str, res_min))) print(' '.join(map(str, res_max))) ``` 希望对你有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

伟大的车尔尼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值