单调队列

66 阅读4分钟

前沿

最近接触到了一种很有意思的数据结构,叫单调队列,可以用来维护一个给定大小的区间的最值,这个区间就像滑动窗口。时间复杂度是O(n),n是给定序列的长度。

一、什么是单调队列

1.1、单调队列的含义

单调队列,顾名思义,是一种具有单调性的队列。单调队列分为单调递增队列和单调递减队列两种。

-单调递增队列:队列的头部元素(左端)是当前区间的最小值,用来维护区间的最小值

-单调递增队列:队列的头部元素(左端)是当前区间的最大值,用来维护区间的最大值

1.2、一个简单的例子

先来看一个简单的单调队列的例子。

序列为:[3 ,1 ,5 , 7 ,4 ,2 , 1],现在要维护区间长度为3的最大值。如下图:

image.png

可以看出,有元素入队列的操作,也有元素出队列的操作。并且,虽然维护的长度是3,但是单调队列的元素个数并不一定总是和窗口的长度是一致的,这是有别于优先队列的地方。因为单调队列只是维护有效的以及有可能有效的最大值

二、单调队列的实现思路

2.1 大体思路

单调队列的实现步骤大概分为三步:“掐头去尾”然后取队头元素。

1.去尾操作。 队尾元素出列。每当有新的元素加入的时候,就要添加到单调队列的队尾(也就是右端进)。然后不断删除影响队列单调性的元素**,遇到小于(大于)新元素的队尾元素就要删除掉。每删除一个队尾元素,就要重新判断新元素是否可以加入队列,直到队尾元素大于等于新元素、或者队列为空为止

2.掐头操作。 队头元素出列。判断头元素(下标)是否在当前待求解的区间之内(因为窗口是不断滑动的),如果不在区间内,就要剔除。直到队头元素满足要求为止。

3.取头元素。 此时,单调队列的头部维护的就是当前区间的最大(小)值,取头元素作为返回值即可。

2.2 具体实现流程

2.2.1 去尾操作:队尾元素出队列

假设需要维护一个 区间长度为L 的最大值,显然,我们需要一个 单调递减队列

现在有一个新元素new(序号为new_id)待放入队列,在新元素new入队列之前,需要先执行下面的操作:

  1. 如果当前 队列为空,则 直接将new放入队列 。否则,执行下一步。
  2. 当前 队列不为空 。(假设队列的尾元素为rear)
  • 如果rear<new,则 尾元素rear出队列,直到 当前队列为空 或者 rear<new不再满足。紧接着,元素new入队列。
  • 如果rear>=new,直接将元素new放入队列。

2.2.2 删头操作:队头元素出队列

将新元素new入队列之后,我们还需要判断当前队列中 队头元素 是否在 待求解的区间范围 内。

假设队列的头元素为front(序号为front_id)。

如果此时当前 队列不为空 ,且 front_id < new_id-L+1 ,那么将 队列头元素front出队列 。不断重复此过程,直至front_id>=new_id-L+1
(也就是说,将队列中序号不在区间[ new_id-L+1,new_id ]的元素删除)

2.2.3 取解操作

经过上面的操作,此时 队列的头元素 就是 区间[ new_id-L+1,new_id] 的最大值。

三、C++代码实现

单调队列的典型题目leetcode链接:239. 滑动窗口最大值 - 力扣(LeetCode)

C++代码实现:

vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int len = nums.size();
        vector<int>res;
        deque<int>que;//单调队列做法,里面放的是数组元素的下标
        //这里必须是下标,因为后面要根据下标来判断队列头部的元素是否还在滑动窗口里面
        for(int i = 0;i < len;i ++)//i代表的是滑动窗口的前端(右端)
        {
            while(!que.empty() && nums[i] > nums[que.back()] )//去尾操作:队尾元素出队列
            {
                que.pop_back();
            }
            que.push_back(i);
            //que.push_back(nums[i]);
            //要时刻谨记que里面元素的含义,que里面保存的是nums数组元素的下标,而不是元素值,一开始这里写错了

            if(i >= k - 1)//当i == k - 1的时候,就说明滑动窗口已经包含k个元素le,单调队列维护的是k个元素的最大值 
            {
                while(!que.empty() && que.front() < i - k + 1)//删头操作:队头元素不符合要求的出队列
                    que.pop_front();
                res.push_back(nums[que.front()]);//取满足条件的当前队列的最大值
            }
        }
        return res;
    }