Version: Next

239.滑动窗口最大值

239. 滑动窗口最大值

难度 困难

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

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

You are given an array of integers nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position.

Return the max sliding window.

示例 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 <= 105
  • -104 <= nums[i] <= 104
  • 1 <= k <= nums.length

时间复杂度: 要求 O(n)

队列

提示

使用双端队列

  • 先确定返回 int[] 的长度,观察一下为 n - k + 1

image-20210405154815648

  • 定义两个指针 wi
    • i:用来扫描数组,定义 i 指向的是 滑动窗口的最后一个元素,因此一开始 w 指向的内存区域是非法的
    • w:指向滑动窗口的第一个元素
    • wi 总是一起移动,它们之间的差值是固定的,为 k - 1
  • 定义一个 双端队列,用来存储数组中的 索引值
    • 队列中的索引,对应的 元素值从头到尾是逐渐 变小
  • 操作:
    • 当队列为空,即添加第一个元素,由 i 指向,直接 尾部入队
    • 当队列不为空,i 指向的元素要与 队列尾部元素 值进行比较
      • while 尾部元素值 <= i 元素值 : 将 原尾部元素值 移除,再让 i 指向的元素值对应索引 尾部入队 (维持 从头到尾 逐渐减小)
      • 尾部元素值 > i 元素值i 直接尾部入队
    • 直到 w 开始指向合法内存地址,表明出现 第一个滑动窗口,那么应当 开始更新最终输出数组的值,此时滑动窗口的最大值,就是 双端队列的头元素。(但要检查队列头的索引是否仍然包含在当前滑动窗口的范围中)
      • 如果已经不在当前滑动窗口范围内,要移除队列头元素
public class _239滑动窗口最大值 {
public static int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0 || k < 1) return new int[0];
if (k == 1) return nums;
// 结果数组
int[] maxArray = new int[nums.length - k + 1];
// 双端队列
Deque<Integer> deque = new LinkedList<>();
for (int i = 0, w = i - k + 1; i < nums.length; i++, w++) {
while (!deque.isEmpty() && nums[deque.getLast()] <= nums[i])
deque.pollLast(); // 不断的从尾部删除较小值
// 直到 尾部元素值 > i 元素值 。 尾部入队
deque.offerLast(i);
if (w >= 0) { // w 开始合法,出现滑动窗口
// 检查队列头是否在滑动窗口内
if (deque.peekFirst() < w || deque.peekFirst() > i)
deque.pollFirst(); // 不在了,就移除它
// 更新结果数组,当前滑动窗口的最大值就是队列头
maxArray[w] = nums[deque.peekFirst()];
}
}
return maxArray;
}
public static void main(String[] args) {
int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
int[] res = maxSlidingWindow(nums, 3);
for (int m : res) {
System.out.println(m + " ");
}
}
}

为什么这样做

  1. 整体的目的是什么
    • 队头的元素是当前滑动窗口中的最大值
    • 让较大的元素尽量向队头靠
  2. 队头的作用是什么
  3. 为什么当 nums[队尾] ≤ nums[i] 时,要将队列中的元素删除

简单粗暴好用法

  • 两个指针 lirili 直接指向 index = 0 的位置,ri = li + k - 1
  • 一上来就扫描 前k个 元素,找出其中的最大值,记录其 索引
  • 移动窗口,只比较新加入的那个元素值,与 先前最大值 比较,决定是否更新最大值
  • 继续移动,判断先前最大值是否还在窗口中,如果已经不在窗口中了,那么重新扫描整个当前窗口,记录 最大值
  • 无脑重复
  • 对乱序序列性能还行,对递减序列而言,效率很差,根暴力法差不多

动态规划