题目描述
给你一个整数数组 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]
题解
分析
该题的解题思路其实很明确
- 维护一个滑动窗口遍历数组
- 每次计算滑动窗口中的最大值
但问题在于如何求滑动窗口中的最大值,因此问题就转换成了如何用最少的时间求一个序列的最大值
一般情况下我们有以下几种算法
- 直接在序列中暴力遍历
- 使用内置sort函数
显然,外部使用滑动窗口遍历数组的时间已经达到了O(n),如果我们采取以上方式计算最大值,那么我们最终的算法时间复杂度必然已经达到了O(n^2)
再次观察,由于我们只需要获取序列的最大值,并不关心序列的顺序,我们要问:
有没有一种数据结构,可以每次将序列元素放进去之后,自动更新最大值,并将最大值放到头部呢?这样我们只需将滑动窗口的数据放到这个数据结构中,然再从这个数据结构的头部取出元素不就成功解决问题了么
而数据结构中恰好有一种数据结构------堆,可以完成这个任务
当我们将一个序列放进一个大顶堆时,堆会自动调整,将最大值放到堆顶,我们只需要获取堆顶的元素即可
因此,算法的思路就变成了
- 维护一个滑动窗口,遍历数组中的元素
- 每次将滑动窗口中的序列放进大顶堆中,大顶堆会自动进行调整,将序列的最大值放到堆顶
而我们知道
遍历数组的时间是O(n),而堆调整的时间是O(log n),因此整个算法的时间复杂度就变成了O(n*log n)
这样我们就得到了以下两种解法
- 维护一个大顶堆
- 使用优先级队列
除此之外,我们完全可以自己维护一个单调队列,这个队列中的元素按照降序排序,队头的元素永远是序列的最大值,这样我们只需要每次从队头中取出元素即可 ,而从队头取出元素的时间为O(1),因此整个的时间复杂度是O(n)
接下来,我们就优先级队列和单调队列进行说明
优先级队列
首先我们需要知道的是
优先级队列的底层数据结构就是堆,因此我们使用优先级队列这种数据结构解题,本质上就是在使用堆
有关优先级队列的使用和介绍可参考
std::priority_queue - cppreference.com
算法思路其实和上述解释的过程是一样的
- 维护滑动窗口,并将滑动窗口中的数值放到优先级队列中
- 优先级队列底层默认使用的是大顶堆,因此会自动进行堆调整,将最大值放到堆顶
- 故只需取出堆顶的元素即可
但问题在于,
- 我们需要保证优先级队列中的数据永远是当前滑动窗口中的序列 ,这样才能保证优先级队列中堆顶元素的最大值是当前滑动窗口的最大值
- 因此我们需要在移动滑动窗口时,将上一个滑动窗口中的元素pop掉
- 但优先级队列中只有堆顶元素是确定的,我们无法确定被pop的元素在队列中的位置
但上述条件其实不需要完全满足,也就是说
- 我们不必确定当前优先级队列中的所有元素都一定是滑动窗口中的元素
- 我们只需要确保当前优先级队列中的队头元素一定是当前滑动窗口中的元素,而不是上一个滑动窗口中的元素即可
所以,
我们在push优先级队列元素的时候,需要将元素在nums中的下标位置也记录下来,用于确定优先级队列中堆顶的元素是否是当前滑动窗口中的元素
故整个代码如下
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
//优先级队列,维护大顶堆
std::priority_queue<std::pair<int,int>> pq;
std::vector<int> res;
//先处理第一个滑动窗口
for(int i=0;i<k;++i)
pq.emplace(nums[i],i);
res.emplace_back(pq.top().first);
//接着遍历剩余的滑动窗口
for(int i=k;i<nums.size();++i){
pq.emplace(nums[i],i);
//只要发现堆顶中元素不在当前滑动窗口中,就将其剔除掉
while(pq.top().second<(i-k+1)){
pq.pop();
}
res.emplace_back(pq.top().first);
}
return res;
}
};
单调队列
首先,我们需要维护一个队列,并保证
- 每次push进去元素时,最大值永远在队头
因此,我们可以这样写
cpp
void push(const int& val){
//只要push的元素val比队尾的元素大,就将其pop掉,直到push的元素val能够放到队头
while(!queue_.empty() && val>queue_.back())
queue_.pop_back();
queue_.emplace_back(val);
}
假如现在有三个元素【1,-1,3】需要依次被push,则整个push过程如下图所示
而队列pop的过程较为简单,如果发现是需要被pop的元素,直接pop掉即可,如下所示
cpp
void pop(const int& val){
if(!queue_.empty() && val==queue_.front())
queue_.pop_front();
}
基于这样的数据结构,于是我们每次只需获取队头元素,即可获取到当前序列的最大值
cpp
int getFront() const {
return queue_.front();
}
完整的单调队列代码如下所示
cpp
class Queue{
public:
void push(const int& val){
while(!queue_.empty() && val>queue_.back())
queue_.pop_back();
queue_.emplace_back(val);
}
void pop(const int& val){
if(!queue_.empty() && val==queue_.front())
queue_.pop_front();
}
int getFront() const {
return queue_.front();
}
private:
std::deque<int> queue_;
};
于是整个解题过程就顺利成章的变为
cpp
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
Queue queue;
std::vector<int> res;
//先处理第一个滑动窗口
for(int i=0;i<k;++i)
queue.push(nums[i]);
res.emplace_back(queue.getFront());
//接下来移动滑动窗口,遍历数组,并将元素放入queue中
for(int i=k;i<nums.size();++i){
//将当前滑动窗口左侧边界的数值剔除掉queue,保证当前queue中维护是当前滑动窗口中的序列
queue.pop(nums[i-k]);
//将当前滑动窗口中的值push进queue中
queue.push(nums[i]);
//由我们维护的单调队列的性质,只需取出队头的元素,即是滑动窗口中的最大值
res.emplace_back(queue.getFront());
}
return res;
}
整个解题代码如下所示
cpp
class Solution {
public:
class Queue{
public:
void push(const int& val){
while(!queue_.empty() && val>queue_.back())
queue_.pop_back();
queue_.emplace_back(val);
}
void pop(const int& val){
if(!queue_.empty() && val==queue_.front())
queue_.pop_front();
}
int getFront() const {
return queue_.front();
}
private:
std::deque<int> queue_;
};
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
Queue queue;
std::vector<int> res;
for(int i=0;i<k;++i)
queue.push(nums[i]);
res.emplace_back(queue.getFront());
for(int i=k;i<nums.size();++i){
queue.pop(nums[i-k]);
queue.push(nums[i]);
res.emplace_back(queue.getFront());
}
return res;
}
};