剑指Offer_#59-I_滑动窗口的最大值
Contents
题目
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
提示:
你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。
思路分析
用一个双端队列维护窗口的最大值,保证双端队列是非严格递减的(前一个元素大于等于后一个元素),那么双端队列的第一个元素正好就是当前窗口当中的最大值。
算法流程
遍历数组,维护两个指针i,j,分别指向窗口左右边界,右指针从第一个元素开始,左指针则始终与右指针相距k-1(中间刚好有k个元素),指针每次移动后执行如下过程:
- i>0时(在此之前窗口还未形成),如果当前队列的头部恰好是nums[i-1],则将其从队列里删除
- 比较nums[j]与队列当中的数字(从后往前看),将小于nums[j]的全部删除(因为nums[j]是当前新加入的数字,比它小的绝不可能是窗口最大值)
- 将nums[j]加入到队列尾部,这时整个队列是非严格递减的,头部的值就是当前窗口的最大值
- 如果i >= 0,将头部数字加入到res中
总结起来,就是维护一个特殊的双端队列数据结构,窗口滑动时,将上个窗口的第一个元素删除,再把上个窗口之后的第一个元素加入进来,且保证队列头部数字最大。
细节问题:
- 指针初始值:
j = 0,i = 0-(k - 1) = 1-k
- 结果数组res的长度:从第k个元素开始,到第n个元素,有几个数字?n-k+1
- java的双端队列:
Deque<Integer> deque = new LinkedList<>();
- peekFirst(),peekLast()
- removeFirst(),removeLast()
- addLast
- 特殊情况:空数组,k = 0,返回空数组
- 删除元素的时候,必须保证队列非空
解答
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
if(n == 0 || k == 0) return new int[0];
Deque<Integer> deque = new LinkedList<>();
//res的长度就是滑动窗口的个数,共有n-k+1个
int[] res = new int[n - k + 1];
//ERROR:不可以写成 int i = 1 - k,int j = 0; 因为这样写是两个statement
for(int i = 1 - k,j = 0; j <= n - 1; i++, j++){
//1.如果当前窗口删除掉的nums[i - 1]恰好时上一个窗口的最大值,在队列中将这个元素删除
if(i >= 1 && deque.peekFirst() == nums[i - 1])
deque.removeFirst();
//2.当前窗口新加入nums[j],小于此值的绝对不会是最大值,所以直接从队列将这些较小的数字删除(从后往前)
//ERROR:这里判断非空要写在前,利用短路特性
while(!deque.isEmpty() && nums[j] > deque.peekLast())
deque.removeLast();
//3.将新加入的nums[j]加入队列末尾
deque.addLast(nums[j]);
//4.当前窗口最大值就是队列头部,将其写入res
if(i >= 0) res[i] = deque.peekFirst();
}
return res;
}
}
复杂度分析
时间复杂度:O(n),遍历整个数组
空间复杂度:O(k),因为双端队列里边的数字始终就是当前窗口的数字,所以队列的大小是k