一、需求
- 给定一个数组
nums
和滑动窗口的大小k
,请找出所有滑动窗口里的最大值。
二、双指针
2.1 思路分析
- 利用双指针分别指向窗口的左右边界,利用一个函数计算边界内的最大值,将返回值存放到结果数组中;
2.2 代码实现
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || nums.length == 0) return new int[0];
//res的长度画个图就能算出来
int[] res = new int[nums.length-k+1];
int i = 0;
int j = k - 1;
int index = 0;
while(j < nums.length) {
res[index] = maxValue(nums, i, j);
i++;
j++;
index++;
}
return res;
}
//该方法求窗口的的最大值
public int maxValue(int[] nums, int i, int j) {
int max = Integer.MIN_VALUE;
for(int k = i; k <= j; k++) {
max = Math.max(max, nums[k]);
}
return max;
}
}
2.3 复杂度分析
- 时间复杂度为O(NK),其中N为数组元素的个数,滑动窗口要滑动N-1+k次,每次查找最大值需要K次,故总体时间复杂度为O(NK);
- 空间复杂度为O(N),每次滑动都要存储一个最大值,故总体消耗O(N)级别的空间;
三、单调队列
3.1 思路分析
- 我们使用一个非严格递减的双端队列,其中队列中只包含窗口中的数据,队头始终为窗口里元素的最大值;
- 使用变量 i , j 来表示窗口的左、右边界,一开始 i 初始化为 1-k,j 初始化为0,即 0 - (1 - k) + 1 == k,就是窗口的大小;
- 然后窗口就开始向右滑动,当 i > 0 时,判断移除窗口的nums[i - 1]是否为最大值(队头元素),如果是,就把队头元素也搞掉,如果不是,那说明最大值还在窗口里,就不用管;
- 向右滑动,nums[ i - 1 ]出来了,但是窗口右侧又进来nums[ j ],此时若队列不为空,则为了保证最大值始终在队头,我们让该队列非严格递减,也就是判断队列队尾的元素与nums[ j ]的大小,若nums[j]更大,那么就把这队尾元素移除,反之把nums[ j ]放在它后面;
- 每次滑动结束后,若当前窗口左边界 i >= 0,取出队头元素值,存放到结果数组 res 中;
- 当滑动窗口完全结束后,返回结果数组 res;
3.2 代码实现
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0 || k == 0 || nums == null) return new int[0];
Deque<Integer> deque = new LinkedList<>();
int[] res = new int[nums.length - k + 1];
for(int j = 0, i = 1 - k; j < nums.length; i++,j++) {
if(i > 0 && deque.peekFirst() == nums[i-1]) {
deque.removeFirst();
}
while(!deque.isEmpty() && deque.peekLast() < nums[j]) {
deque.removeLast();
}
deque.addLast(nums[j]);
if(i >= 0) {
res[i] = deque.peekFirst();
}
}
return res;
}
}
3.3 代码优化(减少冗余判断)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || nums.length == 0 || k == 0) return new int[0];
Deque<Integer> deque = new LinkedList<>();
int[] res = new int[nums.length - k + 1];
//未形成窗口前,假定i为窗口的右边界
for(int i = 0; i < k; i++) {
while(!deque.isEmpty() && deque.peekLast() < nums[i]) {
deque.removeLast();
}
deque.addLast(nums[i]);
}
//保存第1个窗口的最值
res[0] = deque.peekFirst();
//形成窗口后,这里从第2个窗口开始
for(int i = k; i < nums.length; i++) {
//nums[i-k]表示刚从窗口左侧移出的元素
if(deque.peekFirst() == nums[i - k]) {
deque.removeFirst();
}
while(!deque.isEmpty() && deque.peekLast() < nums[i]) {
deque.removeLast();
}
deque.addLast(nums[i]);
res[i - k + 1] = deque.peekFirst();
}
return res;
}
}
3.3 复杂度分析
- 时间复杂度为O(N),其中N为nums的长度,线性遍历nums占用O(N),每个元素最多出队入队一次,因此队列最多消耗O(2N)的复杂度,故总体复杂度为O(N);
- 空间复杂度为O(k),双端队列deque最多存储k个元素,res数组消耗O(k)的额外空间,故总体空间复杂度为O(k);
四、学习地址
作者:Krahets