教你如何使用单调队列解题

开篇

昨天我们介绍了单调栈的解题方法以及适合使用单调栈的几种解题情境。今天我们说的这种也是类似单调栈的另一种特殊数据结构——单调队列。废话不多说,我们直接开始。

单调队列

教你如何使用单调栈中,我们就提到过单调队列这种数据结构。因为队列和栈都是我们熟悉的数据结构,二者非常类似,我们可以使用两个队列去实现一个栈,也可以使用两个栈去实现一个队列,因此单调栈和单调队列差不多。只不过还是最根本的入栈出栈以及入队出队的顺序问题,这个相信不用多提了。
单调队列顾名思义,就是队列内部元素单调的队列?我们都已经有了单调栈这种方便的结构,还需要单调队列有什么用呢?这一数据结构可以解决滑动窗口的一系列问题

滑动窗口问题

首先先看一道leetcode上难度为hard的问题(NO.239):
在这里插入图片描述
搭建解题框架
这道题其实并不复杂,但是题目之所以为hard,是要求我们在O(1)时间内算出每个窗口的最大值,使得整个算法在线性时间完成。在这里我们可以给出一个类似场景结论:
在一堆数字中,已知最值,如果给这堆数添加一个数,那么比较一下就可以了很快算出最值;但是如果减少一个数,就不一定能很快得到最值了,而要遍历所有数重新找最值
回到这道题,每个窗口在前进的的时候,要添加一个数同时减少一个数,所有想在O(1)的时间得到新的最值,就需要使用单调队列这种数据结构。
我们知道一个普通的队列一定有两种操作:push和pop;单调队列的操作也差不多,包含push、pop和max,max函数时返回当前队列中的最大值。
下面我们假设在单调队列中,我们的push、pop、max操作的时间复杂度都为O(1),我们先不管内部具体的实现,先将解题框架搭出来。

#include <vector>
using namespace std;
vector<int> maxSlidingWindow(vector<int>& nums,int k)
{
    MonotonicQueue window;//声明一个单调队列
    vector<int>res;
    for(int i = 0;i < nums.size();i++)
    {
        if(i < k - 1)
        {
            //先把窗口的前k-1填满,表明前k个元素还没全部进入到单调队列中,我们只将其装满一次,剩下可以通过push和pop来维持
            window.push(nums[i]);
        }
        else
        {
            //窗口满了,可以开始向前滑动了
            window.push(nums[i]);
            res.push_back(window.max());
            window.pop(nums[i-k+1]);
            //nums[i-k+1]就是窗口最后的元素
        }
    }
    return res;
}

这个思路很容易理解,我们结合图片再看一下:
在这里插入图片描述
实现单调队列的数据结构
首先我们要认识另一种数据结构:双端队列deque,很简单:

class deque{
	//在对头插入元素n
	void push_front(int n);
	//在队尾插入元素n
	void push_back(int n);
	//在队头删除元素
	void pop_front();
	//在队尾删除元素
	void pop_back();
	//返回队头元素
	int front();
	//返回队尾元素
	int back();
};

这个数据结构是C++ STL的一种标准模板库,没什么难的,底层就是用链表实现的。
单调队列的核心思路和单调栈类似,它的push操作仍然是在队尾添加元素,但是要把前面比新元素小的元素都删掉。

class MonotonicQueue
{
    private:
        deque<int>data;
    public:
        void push(int n)
        {
            while(!data.empty()&&data.back()<n)
                data.pop_back();
            data.push_back(n);
        }
};

可以想象,加入的数字的大小代表人的体重,把前面体重不足的都压扁了,直到遇到更大的量级才能停住。
在这里插入图片描述
如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个单调递减的顺序,因此我们的max()API可以这样写:

int max()
{
	return data.front();
}

pop()API在队头删除元素n:

void pop(int n)
{
    if(!data.empty()&&data.front()==n)
        data.pop_front();
}

之所以要判断data.front()==n,是因为我们想删除的队头元素可能由于很小已经被pop出去了,这个时候就不需要删除了。
在这里插入图片描述
OK,所有的API我们都搞定了,下面附上完整代码:

#include <vector>
#include <deque>
using namespace std;
class MonotonicQueue
{
    private:
        deque<int>data;
    public:
        void push(int n)
        {
            while(!data.empty()&&data.back()<n)
                data.pop_back();
            data.push_back(n);
        }
        int max()
        {
            return data.front();
        }
        void pop(int n)
        {
            if(!data.empty()&&data.front()==n)
                data.pop_front();
        }
};
vector<int> maxSlidingWindow(vector<int>& nums,int k)
{
    MonotonicQueue window;//声明一个单调队列
    vector<int>res;
    for(int i = 0;i < nums.size();i++)
    {
        if(i < k - 1)
        {
            //先把窗口的前k-1填满,表明前k个元素还没全部进入到单调队列中,我们只将其装满一次,剩下可以通过push和pop来维持
            window.push(nums[i]);
        }
        else
        {
            //窗口满了,可以开始向前滑动了
            window.push(nums[i]);
            res.push_back(window.max());
            window.pop(nums[i-k+1]);
            //nums[i-k+1]就是窗口最后的元素
        }
    }
    return res;
}

算法复杂度分析:

很多人包括我自己,一开始都有这样一个疑问,为什么在push中有了一个while循环,可是时间复杂度为O(1)呢,而且到了对于nums中的全部元素来说,时间复杂度是O(n)呢?
没错,单独看一个push操作,时间复杂度的确不是O(1),但是还记得我们昨天在单调栈中说到的整体思想吗?整体上看,对于nums中的每一个元素我们只push和pop一次,没有任何多余的操作,所以时间复杂度就是线性的O(n)。而空间复杂度就是O(k),即为我们题中所指定的窗口大小。

总结

单调队列在添加元素的时候靠删除元素保持队列的单调性,相当于抽取出某个函数中单调递增(或者递减)的部分;而之前说过的优先队列,相当于自动排序。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值