题目描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
提示:
- 1 <= nums.length <= 105
- 104 <= nums[i] <= 104
- 1 <= k <= nums.length
题解
题解1: 单调队列
队列: 队尾进入, 队头弹出。
C++中没有直接支持单调队列,需要我们自己来一个单调队列。
设计单调队列的时候,pop,和push操作要保持如下规则:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
C++中deque是stack和queue默认的底层实现容器(这个我们之前已经讲过啦),deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。
class Solution {
private:
class MyQueue { //单调队列(从大到小)
public:
deque<int> que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
void pop (int value) {
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push (int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
int front() {
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
for (int i = 0; i < k; i++) { // 先将前k的元素放进队列
que.push(nums[i]);
}
result.push_back(que.front()); // result 记录前k的元素的最大值
for (int i = k; i < nums.size(); i++) {
que.pop(nums[i - k]); // 滑动窗口移除最前面元素
que.push(nums[i]); // 滑动窗口前加入最后面的元素
result.push_back(que.front()); // 记录对应的最大值
}
return result;
}
};
** 复杂度分析**
- 时间复杂度: O(n),n为数组长度。
- 空间复杂度: O(k), 定义一个辅助队列。
题解2: 暴力法
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
if(nums.size() == 0) return {};
vector<int> ans;
for(int i = 0; i <= nums.size() - k; i ++){
int maxNum = nums[i];
for(int j = i; j < i + k; j ++){
maxNum = max(maxNum, nums[j]);
}
ans.push_back(maxNum);
}
return ans;
}
};
** 复杂度分析**
- 时间复杂度: O(nk),n为数组长度。
- 空间复杂度: O(1)。
题解3: 动态规划
先将f数组分割成有 k 个元素的块。建立数组 left, 其中 left[j] 是从块的开始到下标 j 最大的元素,方向 左->右。数组 right,其中 right[j] 是从块的结尾到下标 j 最大的元素,方向 右->左。
从左到右遍历数组,建立数组 left。
从右到左遍历数组,建立数组 right。
因为一个窗口 [i, i + k - 1] 最多跨越两个快,所以求窗口中的最大值就是求这个窗口跨越的块中的最大值,我们可以知道,无论是跨越 1 个块也好,2 个块也好,计算处于边界的 i 和 i + k - 1 对应的值就好了,right[i] 是当前块内的滑动窗口从右到左最大的元素,left[i + k - 1] 是当前块内的滑动窗口从左到右最大的元素,这两个范围刚好是整个窗口。
所以窗口的最大值是 max(right[i], left[i + k - 1])。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> ans;
int n = nums.size();
vector<int> left(n, 0), right(n, 0);
int maxNum;
for(int i = 0; i < n; i ++){
if(i % k == 0){
maxNum = nums[i];
}
maxNum = max(maxNum, nums[i]);
left[i] = maxNum;
}
maxNum = nums[n - 1];
for(int i = n - 1; i >= 0; i --){
if(i % k == k - 1){
maxNum = nums[i];
}
maxNum = max(maxNum, nums[i]);
right[i] = maxNum;
}
for(int i = 0; i <= n - k; i ++){
ans.push_back(max(right[i], left[i + k - 1]));
}
return ans;
}
};
** 复杂度分析**
- 时间复杂度: O(n),n为数组长度。
- 空间复杂度: O(n)。
或者
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int length = nums.size();
if(length == 1){
return nums;
}
vector<int> left(length, 0);
left[0] = nums[0];
vector<int> right(length, 0);
right[length - 1] = nums[length - 1];
int j = 0;
for(int i = 1; i < length; i++){
if(i % k == 0){
left[i] = nums[i];
}else{
left[i] = max(left[i - 1], nums[i]);
}
j = length - i - 1;
if((j+1) % k == 0){
right[j] = nums[j];
}else{
right[j] = max(right[j + 1], nums[j]);
}
}
vector<int> res;
int bound = length - k + 1;
for(int i = 0; i < bound; i++){
res.push_back(max(left[i + k - 1], right[i]));
}
return res;
}
};
参考
239. 滑动窗口最大值:【单调队列】详解!
C++ 暴力法/双端队列/dp
简单易懂的动态规划,击败99%+最大堆
滑动窗口最大值
动态规划C++
[Python] Follow-up: 实现二维max pooling