单调队列(Monotonic Queue)是一种特殊的数据结构,可以在常数时间内进行一系列操作,如插入元素、删除元素和获取最大值或最小值。单调队列通常用于解决滑动窗口类问题,其中需要在窗口中维护一些特定性质,例如最大值、最小值或其他聚合函数的值。
它具有以下特性:
-
单调性质: 单调队列中的元素满足一种单调性质,通常是单调递增或单调递减。对于单调递增队列,队列中的元素从前往后依次递增;对于单调递减队列,队列中的元素从前往后依次递减。
-
支持插入和删除: 单调队列支持在队尾插入元素,并且可以在常数时间内删除队首或队尾元素。
-
常数时间获取极值: 单调队列通常用于解决一些需要在滑动窗口中维护一定性质的问题,例如在滑动窗口中查找最大值或最小值。因此,单调队列还支持在常数时间内获取队列中的最大值或最小值。
单调队列,顾名思义,队列必须维护它的单调性
单调队列通常用到deque(双端队列),双端队列内的元素既可以从队首出,也可以从队尾出
单调队列的实现
首先直接从C++库调用deque
deque<int> monotonicQueue; // 定义单调递增队列
接下来,元素(element)入队时,为了维护队列的单调性,我们需要创建一个入队函数(这里以创建一个单调递减的队列为例)
// 向单调递增队列中插入元素
void insertElement(deque<int>& q, int element) {
// 将队列尾部小于当前元素的元素出队
while (!q.empty() && q.back() < element) {
q.pop_back();
}
// 将当前元素入队
q.push_back(element);
}
单调队列基本实现(在单调队列中,删除操作通常不是主要的操作,因为通常单调队列用于求解滑动窗口问题,其中元素的删除是通过滑动窗口的移动实现的)
#include <iostream>
#include <deque>
// 向单调递增队列中插入元素
void insertElement(deque<int>& q, int element) {
// 将队列尾部小于当前元素的元素出队
while (!q.empty() && q.back() < element) {
q.pop_back();
}
// 将当前元素入队
q.push_back(element);
}
int main() {
deque<int> monotonicQueue; // 定义单调递增队列
// 插入一些元素
insertElement(monotonicQueue, 3);
insertElement(monotonicQueue, 5);
insertElement(monotonicQueue, 2);
insertElement(monotonicQueue, 7);
insertElement(monotonicQueue, 1);
return 0;
}
以上就基本实现了单调队列
滑动窗口(Sliding Window)是一种用于解决序列问题的常用技巧,特别是处理数组或链表的连续子序列的问题。该技巧通常用于解决需要在给定大小的窗口范围内对序列进行操作的问题,例如查找最大值、最小值、求和等。
滑动窗口的基本思想是通过定义一个固定大小的窗口,在序列上从左到右滑动,以遍历所有可能的子序列。在每一步中,我们将窗口向右移动一个位置,并在窗口内进行必要的操作(如计算最大值、最小值、求和等)。通过保持窗口的大小固定,我们可以在 O(n) 的时间复杂度内解决许多序列问题,其中 n 是序列的长度。
一个简单的示例来理解滑动窗口的概念。
假设我们有一个长度为 10 的序列
序列: [1, 3, -1, -3, 5, 3, 6, 7, 8, 2]
现在,我们想要找出每个大小为 3 的窗口中的最大值。首先,我们将窗口放在序列的开头,并从左到右移动窗口。
初始状态下,窗口覆盖了序列中的前三个元素:
窗口: [1, 3, -1]
我们可以看到,在这个窗口中,最大值是 3。
接下来,我们将窗口向右移动一个位置,覆盖序列中的下一个三个元素:
窗口: [3, -1, -3]
在这个窗口中,最大值是 3。
依此类推,我们继续向右移动窗口,直到覆盖了整个序列。对于每个窗口,我们都可以计算出其中的最大值。
单调队列和滑动窗口结合在一起可以解决一类特定的问题,例如在滑动窗口中快速找到最大值或最小值。
在滑动窗口问题中,通常需要在给定大小的窗口范围内对序列进行操作,例如查找最大值、最小值、求和等。使用单调队列可以在 O(n) 的时间复杂度内解决这些问题,而不需要使用暴力方法或额外的数据结构。
问题描述:给定一个数组 nums 和一个整数 k,找出数组中每个窗口大小为 k 的子数组的最大值。
分析:如果使用暴力算法,那么时间复杂度就是o(n*k),而单调队列加滑动窗口则是o(n)
接下来,我们来看看代码的实现
单调队列是不会限制队列中数据的范围的,只需要满足单调性即可,而滑动窗口有窗口大小,所以我们需要用特定的东西来限制队列内数据的数量,需要注意的是,此时不能直接将队列内的数据数量与窗口大小比较(如:窗口大小为5,并不是队列中一定要5个元素刚好填满窗口),因为不满足单调性的元素,我们是没有让它入队的,但是它实际应该在窗口中,所以我们需要用元素的下标来表示实际队列的数据数量(就相当于加上没有入队的)
主要代码
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> result; // 用于存储每个窗口的最大值
deque<int> window; // 定义一个双端队列用于存储元素的下标
for (int i = 0; i < nums.size(); ++i) {
// 保持窗口大小不超过 k
if (!window.empty() && window.front() == i - k) {
window.pop_front(); // 如果窗口的首个元素不在当前窗口的范围内,则弹出该元素
}
// 保持窗口内元素单调递减
while (!window.empty() && nums[i] >= nums[window.back()]) {
window.pop_back(); // 弹出队尾元素,保持窗口内元素单调递减
}
// 将当前元素加入窗口
window.push_back(i);
// 当窗口完全覆盖 k 个元素时,记录窗口中的最大值
if (i >= k - 1) {
result.push_back(nums[window.front()]); // 窗口的首个元素即为当前窗口的最大值
}
}
return result; // 返回结果数组
}
全部代码
#include <iostream>
#include <vector>
#include <deque>
using namespace std;
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> result;
deque<int> window;
for (int i = 0; i < nums.size(); ++i) {
if (!window.empty() && window.front() == i - k) {
window.pop_front();
}
while (!window.empty() && nums[i] >= nums[window.back()]) {
window.pop_back();
}
window.push_back(i);
if (i >= k - 1) {
result.push_back(nums[window.front()]);
}
}
return result;
}
int main() {
vector<int> nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
vector<int> result = maxSlidingWindow(nums, k);
cout << "每个窗口大小为 " << k << " 的子数组的最大值为:" << endl;
for (int num : result) {
cout << num << " ";
}
cout << endl;
return 0;
}
单调队列和滑动窗口结合可以解决许多序列处理问题,其中一些常见的问题包括:
-
滑动窗口中的最大值或最小值: 给定一个数组和一个窗口大小,在滑动窗口中找出每个窗口的最大值或最小值。
-
连续子数组的最大和或最小和: 给定一个数组,找出所有连续子数组中的最大和或最小和。
-
滑动窗口中的第 k 大或第 k 小元素: 给定一个数组和一个窗口大小,在滑动窗口中找出每个窗口的第 k 大或第 k 小元素。
-
窗口中的频率统计: 给定一个数组和一个窗口大小,在滑动窗口中统计每个元素出现的频率。
-
滑动窗口中的连续子数组和等于特定值: 给定一个数组和一个窗口大小,在滑动窗口中找出所有连续子数组和等于特定值的情况。
-
滑动窗口中的最长连续子数组: 给定一个数组和一个窗口大小,在滑动窗口中找出最长的连续子数组。
以上列举的问题只是一部分,实际上滑动窗口和单调队列结合可以解决更广泛的序列处理问题,包括字符串处理、动态规划等领域的问题。通过结合使用滑动窗口和单调队列,可以在较低的时间复杂度内解决这些问题。