描述
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。
例如,如果输入数组{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]}
。
窗口大于数组长度的时候,返回空
示例
input:
[2,3,4,2,6,2,5,1],3
output:
[4,4,6,6,6,5]
思路
1、暴力求解:逐窗口遍历
该方法比较容易想到,将每一个滑动窗口遍历,并且依次找到最大值即可。假设滑动窗口大小为m
、数组长度为n
,那么时间、空间复杂度分别为:O(mn)
、O(n)
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
ArrayList<Integer> arr = new ArrayList<>();
if(num.length == 0 || size > num.length || size <= 0)
return arr;
for(int i=0; i<num.length+1-size; i++)
arr.add(getMax(num, i, i+size-1));
return arr;
}
public int getMax(int[] num, int left, int right){
int max = num[left++];
while(left <= right){
if(max < num[left]) max = num[left];
left++;
}
return max;
}
}
2、改进:最大堆
考虑构建一个大小为size
的最大堆,每次从堆顶取最大值,滑动一次则需要判断堆顶元素是否包含在窗口中。
那么每次插入元素时时间复杂度为
O
(
l
o
g
2
(
s
i
z
e
)
)
O(log_2(size))
O(log2(size))、最外层循环了n-size
次;而代码中只定义了一个ArrayList
(内置数组来存储元素,默认的初始大小为10,每次以1.5倍扩容)和PriorityQueue
(内置Object
数组组成的二叉树来存储元素,默认初始大小为11,每次以1.5倍扩容.
因此,时间、空间复杂度分别为为
O
(
n
l
o
g
2
(
s
i
z
e
)
)
O(nlog_2(size))
O(nlog2(size))、O(n)
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
ArrayList<Integer> arr = new ArrayList();
if(num == null || num.length == 0 || size <= 0 || num.length < size)
return arr;
//构建大顶堆,堆顶存储的下标对应的数组元素最大
PriorityQueue<Integer> max_que = new PriorityQueue<>(new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2){
return num[o2] - num[o1];
}
});
//先让最大堆加入第一次滑动窗口大小的元素
for(int i = 0; i < size; i++)
max_que.offer(i);
//对后续的元素进行筛选
for(int i=size; i<num.length; i++){
int top = max_que.peek();//获取堆顶元素,即当前窗口最大值的下标
arr.add(num[top]);//当前窗口最大值加入结果list中
//堆中最大值下标(即最左边)与待加元素下标之差大于滑动窗口长度:表示最大值失效;
//由于循环中需要先poll再peek,因此保证堆中元素个数大于1
if(top + size <= i && max_que.size() > 1){
max_que.poll();
top = max_que.peek();
}
max_que.offer(i);//将当前元素的下标加入最大堆中
}
//由于循环中先调整再加元素,因此需要加入最后一个滑动窗口的最大值
arr.add(num[max_que.poll()]);
return arr;
}
}
3、线性时间复杂度的解法
由于暴力求解的时间复杂度较高,因此自然而然想到能否将时间复杂度降低至O(n)
。在常见的数据结构中,有下列几种解法:
——双端(单调)队列
这个方法主要需要保证:每一次的移动后,队列中的元素从左到右(从前到后)是严格递减的。主要思路:
(1)若当前元素比左侧元素大时,则去掉前面比它小的元素,直到遇到前面比它大的元素或者队列为空
(2)将当前元素加入到队列尾部(即后边)
(3)如果队列最后的元素下标和最前的元素下标超出滑动窗口长度,那么需要移除最前面的元素
(4)确保队列中最前面的元素下标比滑动窗口大,才会有最大值
由于本题中给定的是数组,可以通过下标直接取值,因此队列只需要存储数组的下标就可以了。
以本题中的{2,3,4,2,6,2,5,1}
为例
滑动次数 | 滑动窗口 | 队列中的元素 | 最大值 | 对应的下标 | 说明 |
---|---|---|---|---|---|
1 | 2 | 2 | 滑动窗口长度不够,无最大值 | ||
2 | 2,3 | 3 | 滑动窗口长度不够,无最大值;当前元素3 比队列中的2 大,因此舍弃前面的元素 | ||
3 | 2,3,4 | 4 | 4 | 2 | 当前元素4 比队列中的3 大,因此舍弃前面的元素 |
4 | 3,4,2 | 4,2 | 4 | 2 | 当前元素2 比队列中的4 小,直接加入队列尾部 |
5 | 4,2,6 | 6 | 6 | 4 | 当前元素6 比队列中的(4,2) 大,因此舍弃前面的元素 |
6 | 2,6,2 | 6,2 | 6 | 4 | 当前元素2 比队列中6 小,直接加入队列尾部 |
7 | 6,2,5 | 6,5 | 6 | 4 | 当前元素5 处于队列(6,2) 的中间,因此去掉前面的2 ,然后加入队列尾部 |
8 | 2,5,1 | 5,1 | 5 | 6 | 当前元素1 比队列中的(6,5) 小,加入队列尾部,此时队列(6,5,1) ;但是此处队首元素6 下标为4 ,队尾元素1 下标为7 ,差值7-4+1>3 ,需去掉队首! |
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
ArrayList<Integer> arr = new ArrayList();
if(num == null || num.length == 0 || size <= 0 || num.length < size)
return arr;
Deque<Integer> deque = new LinkedList();
for(int i=0; i<num.length; i++){
//当前值插入队列尾部前,先从右到左删除尾部小的元素
while(!deque.isEmpty() && num[deque.peekLast()] <= num[i])
deque.pollLast();
deque.offerLast(i);//当前值的下标加入到队列尾部
//若当前值加入后,队列最右边元素和最左边元素超过窗口长度,则左边出队列
if(deque.peekFirst() + size == i)
deque.pollFirst();
//当前元素的下标大于或等于窗口长度时,才会有最大值
if(i >= size-1) arr.add(num[deque.peekFirst()]);
}
return arr;
}
}
——动态规划
思路见:动态规划 滑动窗口最大值
该方法的思路是以滑动窗口的大小m
将数组划为n/m
块,left
数组将记录每一个块中当前元素与其左侧元素的最大值,即每一个块内保持从左到右的递增;right
数组则记录每一个块中当前元素与其右侧元素的最大值,即每一个块内保持从右到左的递增。以数组{2,3,4,2,6,2,5,1}
为例
(1)由于m = 3
,因此数组将被分为3块
:[2, 3, 4], [2, 6, 2], [5, 1]
。
(2)left
数组取值为:[2, 3, 4], [2, 6, 6], [5, 5]
(3)right
数组取值为:[4, 4, 4], [6, 6, 2], [5, 1]
滑动窗口可能在一个块内,也可能在不同块里面,那么
(1)滑动窗口在一个块内时,如[0, m-1]
,由于left
数组块内最右侧值最大,right
数组块内左侧值最大,因此,滑动窗口的最大值为left[m-1]
或者right[0]
(2)滑动窗口落在两个块中时,如[k, m+k-1]
= [k, m-1]
+ [m, m+k-1] (k>0)
。由于left
数组块内最右侧值最大,right
数组块内左侧值最大,因此[k, m-1]
的最大值可以取right[k]
,[m, m+k-1]
的最大值可以取left[m+k-1]
,因此滑动窗口的最大值为max{ right[k], left[m+k-1] }
(3)综上述,可知滑动窗口为[i, j]
时,最大值为max{ left[j], right[i] }
import java.util.*;
import java.util.Comparator;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
ArrayList<Integer> arr = new ArrayList();
if(num == null || num.length == 0 || size <= 0 || num.length < size)
return arr;
int n = num.length, j;
int[] left = new int[n];
int[] right = new int[n];
for(int i=0; i<n; i++){
//i表示left数组的索引,从左到右,块内递增
if(i%size == 0) left[i] = num[i];
else left[i] = Math.max(left[i-1], num[i]);
//j表示right数组的索引,从右到左,块内递增
j = n - i - 1;
if(j%size == 0 || j == n-1) right[j] = num[j];
else right[j] = Math.max(right[j+1],num[j]);
}
for(int i=0; i<=n-size; i++){
arr.add(Math.max(left[i+size-1], right[i]));
}
return arr;
}
}