单调队列,滑动窗口
单调队列顾名思义就是队列中元素递增或者递减,元素入队出队时需维护队列的单调性。同单调栈一样,为了保证栈的单调性,在单调减栈中,如果遇到待入栈元素比栈顶大时,需要一直弹出栈顶元素后入栈。单调队列的情形类似,在单调递减队列中,如果遇到待入队元素比队尾大,需要一直让队尾元素出队直到队尾大于待入队元素。
LeetCode 239. 滑动窗口最大值(单调队列)
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 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
分析:
- 暴力法求解需要O(nk)时间
- 使用单调队列这种数据结构,可以牺牲一点空间复杂度O(k),把时间复杂度降到O(n)
维护单调减队:
入队:
- 如果 queue 为空,直接把元素索引放入队尾,进行下一次遍历
- 如果 queue 不为空,查询队尾存放的下标 j ,如果 nums[j] > nums[i], 则 i 直接入队 ;如果 num[j] <= nums[i],则弹出 j,继续进入入队操作。
出队:
如果 queme 的队首存放的下标 j == i - k,说明当前 queue 的最大值已经不是该窗口下的最大值,次大值才是,队首应该出队。
这道题的入队出队情况:(这个例子没有队首出队的情况)
i | nums[i] | nums []表示窗口 | 单调减队 queue | 最大值 |
---|---|---|---|---|
0 | 1 | [1] 3 -1 -3 5 3 6 7 | 1 | - |
1 | 3 | [1 3] -1 -3 5 3 6 7 | 3 | - |
2 | -1 | [1 3 -1] -3 5 3 6 7 | 3 -1 | 3 |
3 | -3 | 1 [3 -1 -3] 5 3 6 7 | 3 -1 -3 | 3 |
4 | 5 | 1 3 [-1 -3 5] 3 6 7 | 5 | 5 |
5 | 3 | 1 3 -1 [-3 5 3] 6 7 | 5 3 | 5 |
6 | 6 | 1 3 -1 -3 [5 3 6] 7 | 6 | 6 |
7 | 7 | 1 3 -1 -3 5 [3 6 7] | 7 | 7 |
java 代码:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
Deque<Integer> queue = new ArrayDeque<>();
for(int i = 0; i < nums.length; i++) {
// 元素入队
while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]) {
queue.removeLast();
}
queue.addLast(i);
// 队首出队
// 如果队首放的索引大于 i - k,说明当前的最大值已经不是该窗口下的最大值,次大值才是
if(i >= k && queue.peekFirst() == i - k) {
queue.removeFirst();
}
// 记录返回值
if(i >= k - 1) {
res[i - k + 1] = nums[queue.peekFirst()];
}
}
return res;
}
}
(待更新,2020.06.11)
LeetCode 209.长度最小的子数组 (滑动窗口)
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的连续子数组,返回 0。
示例:
输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
进阶:如果你已经完成了O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
- 暴力法:时间复杂度O(n^3),空间复杂度O(1)
- 前缀和法:时间复杂度O(n^2),空间复杂度O(n)
- 滑动窗口法:时间复杂度O(n),空间复杂度O(1)
- 二分查找法:时间复杂度O(nlogn) (待尝试)
分析:
对于连续子数组查找值的问题,可以使用一个长度可变的窗口,计算该窗口内的数组元素的和 sum,当 sum 小于目标值时,窗口向右生长框进更多的元素,当 sum 大于目标值时,窗口的左边界缩减使 sum 减小,为了继续向右考察更多的元素值。需要注意的是,这里定义窗口的区间为左闭右闭。
Java 代码:
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int l = 0, r = -1; // 维护一个[l, r]的滑动窗口,r = -1,保证一开始窗口为空
int sum = 0;
int res = 0;
while(l < nums.length) { // 只要l < nums.length,说明后面还可能有更小的窗口
if(r + 1 < nums.length && sum < s) {
sum += nums[++r];
} else { // 当 r 已经到达数组最右端 或 sum >= s 时缩小滑动窗口左边界
if(sum >= s) {
res = res == 0 ? r - l + 1 : Math.min(res, r - l + 1);
}
sum -= nums[l++];
}
}
return res;
}
}