[2021校招必看之Java版《剑指offer》-64] 滑动窗口的最大值

1、题目描述

  【JZ64】给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组 {2,3,4,2,6,2,5,1} 及滑动窗口的大小 3 ,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4,4,6,6,6,5} ;
  针对数组 {2,3,4,2,6,2,5,1} 的滑动窗口有以下 6 个:
   { [2,3,4] ,2,6,2,5,1}, {2, [3,4,2] ,6,2,5,1}, {2,3, [4,2,6] ,2,5,1},
   {2,3,4, [2,6,2] ,5,1}, {2,3,4,2, [6,2,5] ,1}, {2,3,4,2,6, [2,5,1] }。
  知识点:数组,队列
  难度:☆☆

2、解题思路

2.1 暴力遍历

  已知数组 num[] 和窗口大小 size,直接滑动遍历。步骤如下:
  1、定义一个 ArrayList<Integer> 保存每一个滑动窗口的最大值;
  2、遍历 num[] ,假设遍历到索引 i,那么滑动窗口的左边界索引为 i,右边界索引为 i + size - 1;
  3、遍历 num[i] ~ num[i+size-1],找出最大值保存到 list 中。

2.2 右边界遍历

  num = {2,3,4,2,6,2,5,1},size = 3
  暴力遍历存在大量的重复比较,例如窗口 [2,3,4] 和窗口 [3,4,5] 都存在判断 3 < 4,重复计算。

  对于第 2 个元素 4 来说,它是窗口 [2,3,4] 的右边界,是窗口 [3,4,5] 中间的一个值,也是窗口 [4,5,6] 的左边界。
  也就是说,当第 size - 1 个元素之后,每一个元素都是一个窗口的右边界。

  定义一个双向队列 dequeue ,开始遍历 num 的每一个元素:
  1、如果 dequeue 为空,num[i] 入队;
  2、如果 dequeue 不为空,则判断 dequeue 的末端元素和 num[i] 的大小,如果 num[i] 大,就删除 dequeue 的末端元素。重复第 2 步,直到 dequeue 为空,回到第 1 步;或者 num[i] 小,则 num[i] 入队。
  3、每遍历第 i 个元素,判断 dequeue 的首端元素是否过期(即首端元素在 num 中的索引 index 到 i 之前是否超出一个窗口的界限),如果过期则删除首端元素。
  4、当 i + 1 >= num 之后,每一个 num[i] 都是一个窗口的右边界,而经过上面三步才做可以保证 dequeue 里面的元素是递减的,最大值就是 dequeue 的首端元素。

3、解题代码

3.1 暴力遍历

package pers.klb.jzoffer.hard;

import java.util.ArrayList;
import java.util.Arrays;

/**
 * @program: JzOffer2021
 * @description: 滑动窗口的最大值
 * @author: Meumax
 * @create: 2020-07-18 10:47
 **/
public class MaxInWindows {

    public ArrayList<Integer> maxInWindows(int[] num, int size) {
        ArrayList<Integer> list = new ArrayList<>();

        if (num.length < size || size == 0) {
            return list;
        }

        int numOfWindows = num.length - size + 1;    // 窗口的数量

        // 每一个滑动窗口的取值为:num[i], num[i+1], ..., num[i+size-1]
        for (int i = 0; i + size - 1 < num.length; i++) {
            int j = i + size - 1;   // 每一个滑动窗口的右边界索引
            int max = num[j];
            // 遍历滑动窗口,找到最大值
            for (int k = i; k < j; k++) {
                if (max < num[k]) {
                    max = num[k];
                }
            }
            list.add(max);
        }

        return list;
    }
}

  时间复杂度:O(n*k), 其中n为数组大小,k为窗口大小
  空间复杂度:O(1),存结果必须要开的数组不算入额外空间

3.2 右边界遍历

package pers.klb.jzoffer.hard;

import java.util.*;

/**
 * @program: JzOffer2021
 * @description: 滑动窗口的最大值
 * @author: Meumax
 * @create: 2020-07-18 10:47
 **/
public class MaxInWindows {

    public ArrayList<Integer> maxInWindows(int[] num, int size) {
        ArrayList<Integer> list = new ArrayList<>();
        if (num.length < size || size == 0) {
            return list;
        }

        // 双向队列,元素为 num 的索引
        Deque<Integer> queue = new ArrayDeque<>();

        // 遍历数组 num
        for (int i = 0; i < num.length; i++) {
            while (!queue.isEmpty() && num[queue.getLast()] < num[i]) {
                // 如果队列不为空且队列最后一个元素值对应的num小于当前遍历到的num
                // 则队列最后一个元素索引就没有利用价值了
                queue.removeLast();
            }

            // 当前索引入队
            queue.add(i);

            // 滑动窗口的左边界是 queue.getFirst()
            // 滑动窗口的右边界是 queue.getFirst() + size
            // queue 保存的是一个滑动窗口内的索引
            if (queue.getFirst() + size <= i) {
                queue.removeFirst();
            }

            // i + 1 大于等于窗口大小之后的每一个元素都是一个窗口的右边界
            // 经过上面几步,queue 里面的元素一定是递减的
            if (i + 1 >= size) {
                list.add(num[queue.getFirst()]);
            }
        }

        return list;
    }
}

  时间复杂度:O(n), 其中n为数组大小
  空间复杂度:O(k),k为窗口的大小

4、解题心得

  暴力遍历复杂度高的原因往往就是因为存在大量的重复计算,针对重复计算有很多种优化方法,比如把已经计算过的结果进行保存,当下一次需要再计算时直接调用上次的结果即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值