提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
BM45 滑动窗口的最大值
题目描述
给定一个长度为 n 的数组 nums 和滑动窗口的大小 size ,找出所有滑动窗口里数值的最大值。
例如,如果输入数组{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]}。
数据范围: 1 \le size \le n \le 100001≤size≤n≤10000,数组中每个元素的值满足 |val| \le 10000∣val∣≤10000
要求:空间复杂度 O(n),时间复杂度 O(n)
方法一
求各个滑动窗口的最大值,我首先想到的一种简单直观的方法就是逐一求取每个滑动窗口的最大值,即将每次移动后的窗口当作全新的数据段,逐一比较段内各元素的值来找出最大值
1.Java代码
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
if(num==null || num.length==0){
return null;
}
ArrayList<Integer> max_nums = new ArrayList<Integer>();
int window_begin = 0;
//逐一检查所有滑动窗口
while(window_begin+size<=num.length){
int max = num[window_begin];
int window_num_cur = window_begin;
//获取每个滑动窗口内的最大元素
while(window_num_cur<window_begin+size){
if(max<num[window_num_cur]){
max = num[window_num_cur];
}
window_num_cur++;
}
max_nums.add(max);
window_begin++;
}
return max_nums;
}
}
2.算法分析
以上代码简单易懂,易于实现,但缺点是时间复杂度较大,达到O(nm),属于暴力破解,一旦数据队列和滑动窗口大小扩大,算法效率低下的劣势将暴露无遗。
方法二
尝试基于滑动窗口构造一个初始有序的线性结构,且每次滑动需要将左侧元素移出窗口,右侧元素移入窗口,且移入之前需设法在右端进行操作使得该线性结构保持有序,以便快速获取当前窗口最大值,故可使用双向队列满足这一需求。
1.双向队列
Java类库已经提供双向队列结构ArrayDeque供我们直接使用,现基于ArrayDeque构造一个有序队列,在窗口滑动时,左侧元素从队首出队,右侧元素从队尾入队
2、Java代码
第一步:构造有序队列
ArrayList<Integer> max_nums = new ArrayList<Integer>();
//设置一个双向队列,队尾用于插入滑动窗口右侧的新元素,队首抛出旧元素
ArrayDeque<Integer> window_que = new ArrayDeque<Integer>();
for(int i=0; i<size; i++){//第一个窗口的范围为[0,size-1]
/*num数组从左往右,各个元素入队前,
需先让从双向队列队尾开始小于当前待入队元素的元素全部出队,之后新元素再入队
使得队列从队尾到队首递增,且被出队元素在num中的下标小于此时新元素下标,
这些被出队元素之后也必然会比此时入队的新元素先离开滑动窗口,
故不会对之后滑动窗口的最大值判定造成影响
*/
while(!window_que.isEmpty() && num[window_que.getLast()]<num[i]){
window_que.pollLast();
}
window_que.add(i);//新元素下标从队尾入队
}
//获取首个滑动窗口的最大值(位于队首)
max_nums.add(num[window_que.getFirst()]);
第二步:开始窗口滑动操作,每次滑动后都必须保持队列有序
//滑动窗口范围为[i-size+1, i]
for(int i=size; i<num.length; i++){
/*将窗口滑动后被排除在左侧外部的那个元素出队,
即将下标小于 i-size+1 的元素从队首出队
注意:由于队列为了保持有序而只存储滑动窗口部分元素,
即队首存储的元素下标未必是刚刚被移出滑动窗口的元素的下标,
故要判断其下标值是否小于i-size+1,若小于,则进行一次出队操作
*/
if(!window_que.isEmpty() && window_que.getFirst()<(i-size+1)){
window_que.pollFirst();
}
//新元素从队尾入队前,为了保持递增序同样要让队尾所有小于新元素的元素下标从队尾出队
while(!window_que.isEmpty() && num[window_que.getLast()]<num[i]){
window_que.pollLast();
}
window_que.add(i);//新元素下标从队尾入队
//获取当前滑动窗口中的最大值,window_que.getFirst()获取有序队列的队首下标
max_nums.add(num[window_que.getFirst()]);
}
完整代码
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
if(num==null || num.length==0){
return null;
}
ArrayList<Integer> max_nums = new ArrayList<Integer>();
//设置一个双向队列,队尾用于插入滑动窗口右侧的新元素,队首抛出旧元素
ArrayDeque<Integer> window_que = new ArrayDeque<Integer>();
for(int i=0; i<size; i++){//第一个窗口的范围为[0,size-1]
/*num数组从左往右,各个元素入队前,
需先让从双向队列队尾开始小于当前待入队元素的元素全部出队,之后新元素再入队
使得队列从队尾到队首递增,且被出队元素在num中的下标小于此时新元素下标,
这些被出队元素之后也必然会比此时入队的新元素先离开滑动窗口,
故不会对之后滑动窗口的最大值判定造成影响
*/
while(!window_que.isEmpty() && num[window_que.getLast()]<num[i]){
window_que.pollLast();
}
window_que.add(i);//新元素下标从队尾入队
}
//获取首个滑动窗口的最大值(位于队首)
max_nums.add(num[window_que.getFirst()]);
//滑动窗口范围为[i-size+1, i]
for(int i=size; i<num.length; i++){
/*将窗口滑动后被排除在左侧外部的那个元素出队,
即将下标小于 i-size+1 的元素从队首出队
注意:由于队列为了保持有序而只存储滑动窗口部分元素,
即队首存储的元素下标未必是刚刚被移出滑动窗口的元素的下标,
故要判断其下标值是否小于i-size+1,若小于,则进行一次出队操作
*/
if(!window_que.isEmpty() && window_que.getFirst()<(i-size+1)){
window_que.pollFirst();
}
//新元素从队尾入队前,为了保持递增序同样要让队尾所有小于新元素的元素下标从队尾出队
while(!window_que.isEmpty() && num[window_que.getLast()]<num[i]){
window_que.pollLast();
}
window_que.add(i);//新元素下标从队尾入队
//获取当前滑动窗口中的最大值,window_que.getFirst()获取有序队列的队首下标
max_nums.add(num[window_que.getFirst()]);
}
return max_nums;
}
}
3、算法分析
方法二的时间复杂度为O(n),不排除在某些情况下会下降至O(nm),但平均效率比起方法一已有显著提升