LeetCode 239. 滑动窗口最大值
1、题目
题目链接:239. 滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k_ _的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 _滑动窗口中的最大值 _。
示例 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]
提示:
- 1 <= nums.length <= 105
- -104 <= nums[i] <= 104
- 1 <= k <= nums.length
2、单调队列
思路
为了解决这个问题,我们可以使用双端队列(deque)来存储滑动窗口中的元素索引。双端队列的特点是可以在两端添加或删除元素,非常适合处理滑动窗口问题。
我们可以使用双端队列(deque)来实现一个单调递减队列,即队列的头部元素始终是窗口中的最大值。
设计单调队列:
void pop(int val)
:如果队列不为空,并且val
等于队列的头部元素,则弹出队列的头部元素。void push(int val)
:当插入一个新的元素val
时,这个方法会删除队列中所有小于val
的元素,以确保队列的头部始终是窗口中的最大值。然后,它将val添加到队列的尾部。int front()
:返回队列头部的元素,即当前窗口的最大值。
接下来使用这个单调递减队列来解决滑动窗口最大值问题。遍历数组,对于每个窗口,维护一个单调递减队列,队列的头部元素始终是窗口中的最大值。
- 首先遍历
nums
的前k
个元素,并使用que.push
方法将它们添加到队列中。这样,队列que
就包含了nums
的前k
个元素,并且其头部是这些元素中的最大值。 - 接下来,它将队列头部的元素(即前
k
个元素的最大值)添加到result
中。 - 然后,对于
nums
中的剩余元素(从索引k开始),它执行以下操作:- 使用
que.pop
方法删除窗口中的第一个元素(即nums[i - k]
)。 - 使用
que.push
方法将新元素nums[i]
添加到队列中。 - 将队列头部的元素(即当前窗口的最大值)添加到
result
中。
- 使用
- 最后,函数返回结果
result
。
示例
假设nums = [1, 3, -1, -3, 5, 3, 6, 7],k = 3。
- 初始窗口为[1, 3, -1],最大值为3。
- 滑动窗口为[3, -1, -3],最大值为3。
- 滑动窗口为[-1, -3, 5],最大值为5。
- 滑动窗口为[-3, 5, 3],最大值为5。
- 滑动窗口为[5, 3, 6],最大值为6。
- 滑动窗口为[3, 6, 7],最大值为7。
因此,函数将返回[3, 3, 5, 5, 6, 7]。
代码
#include <iostream>
#include <vector>
#include <deque>
using namespace std;
class Solution {
private:
//单调递减队列(从大到小),即队列的头部元素始终是窗口中的最大值。
class MyQueue {
public:
// 使用 deque 来实现单调队列
deque<int> que;
// 如果队列不为空,并且 val 等于队列的头部元素,则弹出队列的头部元素
void pop (int val) {
if (!que.empty() && val == que.front()) {
que.pop_front();
}
}
// 向队列中添加元素 val,如果 val 大于队列的尾部元素,则弹出队列的尾部元素,直到 push 的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push (int val) {
while (!que.empty() && val > que.back()) {
que.pop_back();
}
que.push_back(val);
}
// 返回队列头部的元素,即当前窗口的最大值。
int front() {
return que.front();
}
};
public:
// 使用单调递减队列来解决滑动窗口最大值问题。
// 遍历数组,对于每个窗口,维护一个单调递减队列,队列的头部元素始终是窗口中的最大值。
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
// 遍历 nums 的前 k 个元素,并使用 que.push 方法将它们添加到队列中。这样,队列 que 就包含了 nums 的前 k 个元素,并且其头部是这些元素中的最大值。
for (int i = 0; i < k; i++) {
que.push(nums[i]);
}
// 将队列头部的元素(即前 k 个元素的最大值)添加到 result 中。
result.push_back(que.front());
// 遍历 nums 的后续元素
for (int i = k; i < nums.size(); i++) {
// 弹出当前窗口的第一个元素 nums[i - k]
que.pop(nums[i - k]);
// 将新元素 nums[i] 添加到队列中
que.push(nums[i]);
// 将当前窗口的最大值添加到 result 中
result.push_back(que.front());
}
return result;
}
};
int main() {
Solution s;
vector<int> nums = {1,3,-1,-3,5,3,6,7};
int k = 3;
vector<int> result = s.maxSlidingWindow(nums, k);
for (int i = 0; i < result.size(); i++) {
cout << result[i] << " ";
}
cout << endl;
return 0;
}
复杂度分析
- 时间复杂度: O(n)
- 空间复杂度: O(k)
3、使用 multiset 作为单调队列
思路
使用 multiset 作为单调队列
多重集合 (multiset) 用于存储当前窗口中的元素。multiset 允许存储重复的元素,并且元素在容器中是有序的(默认按升序排列)。
在遍历原数组的时候,只需要把窗口的头元素加入到 multiset 中,然后把窗口的尾元素删除即可。因为 multiset 是有序的,并且提供了 *rbegin(),可以直接获取窗口最大值。
- 通过
for
循环遍历nums
中的每个元素。 if (i >= k) st.erase(st.find(nums[i - k]))
: 如果当前索引i
大于或等于k
,则从st
中删除i - k
索引处的元素。这是为了确保st
中只包含当前窗口内的元素。st.insert(nums[i])
: 将当前元素插入到st
中。if (i >= k - 1) ans.push_back(*st.rbegin())
: 如果当前索引i
大于或等于k - 1
,则将st
中的最大元素(即st.rbegin()
指向的元素)添加到ans
中。这是因为当窗口中有k
个元素时,我们可以确定窗口的最大值。
代码
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
// 创建一个 multiset 容器 st,用于存储滑动窗口中的元素
multiset<int> st;
// 创建一个 vector 容器 ans,用于存储每个滑动窗口的最大值
vector<int> ans;
// 遍历 nums 数组
for (int i = 0; i < nums.size(); i++) {
// 如果当前位置 i 大于等于 k,说明滑动窗口的大小已经达到 k,需要移除窗口最左边的元素
if (i >= k) st.erase(st.find(nums[i - k]));
// 将当前位置的元素插入到 multiset 容器中
st.insert(nums[i]);
// 如果当前位置 i 大于等于 k - 1,说明滑动窗口的大小已经达到 k - 1,可以计算窗口的最大值
if (i >= k - 1) ans.push_back(*st.rbegin());
}
// 返回每个滑动窗口的最大值
return ans;
}
};
复杂度分析
- 时间复杂度: O(n)
- 空间复杂度: O(k)