题目描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回每个滑动窗口中的最大值数组,时间复杂度要求O(n)
示数例 1:
输入: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
示例 2:
输入:nums = [1], k = 1
输出:[1]
示例 3:
输入:nums = [1,-1], k = 1
输出:[1,-1]
示例 4:
输入:nums = [9,11], k = 2
输出:[11]
示例 5:
输入:nums = [4,-2], k = 2
输出:[4]
解题思路
为什么要用单调队列
对于每个滑动窗口,我们可以使用 O(k)的时间遍历其中的每一个元素,找出其中的最大值。对于长度为 n 的数组nums 而言,窗口的数量为 n-k+1,因此该算法的时间复杂度为O((n−k+1)k)=O(nk),会超出时间限制,因此我们需要进行一些优化。
引入一个单调递减的队列(左边对头数据最大,右边队尾数据最小)的目的是为了存储每次滑动窗口中最大值。
为什么用单调队列而不是单调栈
因为我们需要保持队列的两端都可以访问和修改。当新元素加入时,我们需要从队列后端移除所有比新元素小的元素,以保持队列的单调递减;同时,当窗口移动时,我们可能需要从队列前端移除旧的元素(即不再在当前窗口内的元素)。
详细的思路
1.想将我们第一个窗口的k个值存入单调双端队列中,单调队列里面的值为单调递减的。如果发现队尾元素小于等于要加入的元素,则将队尾元素出队,直到队尾元素大于新元素时,再让新元素入队,目的就是维护一个单调递减的队列。
2.我们将第一个窗口的所有值,按照单调队列的规则入队之后,因为队列为单调递减,所以队头元素必为当前窗口的最大值,则将队头元素添加到数组中。
3.然后把剩余的nums.length()-k个数按照规则进行入队,维护单调递减队列,同时剔除已经不在滑动窗口里的数据。
4.每次将队头元素存到返回数组里。
5.返回数组
编码实现
Java代码
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length;
if (len == 0) {
return nums;
}
int[] arr = new int[len - k + 1];
int arr_index = 0;
// 我们需要维护一个单调递增的双向队列
Deque<Integer> deque = new LinkedList<>();
// 先将第一个窗口的值按照规则入队
for (int i = 0; i < k; i++) {
while (!deque.isEmpty() && deque.peekLast() < nums[i]) {
deque.removeLast();
}
deque.offerLast(nums[i]);
}
// 存到数组里,队头元素
arr[arr_index++] = deque.peekFirst();
// 移动窗口
for (int j = k; j < len; j++) {
//窗口的前一个(即不在窗口里的元素)元素等于队头元素
if (nums[j - k] == deque.peekFirst()) {
deque.removeFirst();
}
while (!deque.isEmpty() && deque.peekLast() < nums[j]) {
deque.removeLast();
}
deque.offerLast(nums[j]);
arr[arr_index++] = deque.peekFirst();
}
return arr;
}
}
python代码
from collections import deque
# 为什么用单调队列而不是单调栈?因为我们需要保持队列的两端都可以访问和修改。当新元素加入时,我们需要从队列后端移除所有比新元素小的元素,以保持队列的单调递减;同时,当窗口移动时,我们可能需要从队列前端移除旧的元素(即不再在当前窗口内的元素)。
# 从队头到队尾保持单调递减
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if not nums:
return []
if k == 1:
return nums
window = deque()
result = []
for i, num in enumerate(nums):
# 从队列尾部移除小于当前元素的所有元素
# 这种方法保证了队列的头部始终是当前窗口的最大值。当新元素加入时,所有小于它的元素都不会影响后续窗口的最大值计算,因为这些较小的元素永远不会成为窗口的最大值(只要包含当前较大的元素)
while window and nums[window[-1]] < num:
window.pop()
# 将当前元素索引加入队尾
window.append(i)
# 口已经向右移动,当 window[0] 等于 i - k 时, 原来的之前窗口的最大值还霸占着队头就要移除掉
if window[0] == i - k:
window.popleft()
# 一旦窗口形成,添加最大值到结果列表
if i >= k - 1:
result.append(nums[window[0]])
return result
相似题目
LeetCode209之长度最小的子数组(相关话题:滑动窗口,前缀和,二分)_int target = s + sums[i - 1];-CSDN博客