【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] }。
知识点:数组,队列
难度:☆☆
已知数组 num[] 和窗口大小 size,直接滑动遍历。步骤如下:
1、定义一个 ArrayList
2、遍历 num[] ,假设遍历到索引 i,那么滑动窗口的左边界索引为 i,右边界索引为 i + size - 1;
3、遍历 num[i] ~ num[i+size-1],找出最大值保存到 list 中。
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 的首端元素。
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),存结果必须要开的数组不算入额外空间
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为窗口的大小
暴力遍历复杂度高的原因往往就是因为存在大量的重复计算,针对重复计算有很多种优化方法,比如把已经计算过的结果进行保存,当下一次需要再计算时直接调用上次的结果即可。