一、滑动窗口
1.1 定义
滑动窗口实际上是数组中两个指针之间的区域。两个指针分别为左指针 L 和右指针 R ,两个指针均是从左往右移动,但使用时需要确保 R 始终在 L 的右侧;可以将 R 指针的右移理解为窗口的更新,将 L 指针的右移理解为移除窗口内的过期元素。
为了实现能快速找到滑动窗口内的最大值,我们需要维护一个单调递减的窗口。例如[4,3,5,4,3]这个数组,首先R右移,元素4进入窗口;R右移,元素3进入窗口;R右移,元素5进入窗口后发现单调关系被破坏,所以 L 右移两次,元素“4”,“3”离开窗口,此时窗口内就只有“5”,单调关系不变;然后 R 右移,元素4进入窗口,单调关系不变;R右移,元素3进入窗口,结束。通过这个环节,我们可以很容易地找到滑动窗口中的最大值;此外,也可以设计滑动窗口为单调递增的,解决问题方式同理。
每个数据仅进队列一次,出队列一次。时间复杂度为
1.2 例题:生成窗口最大值数组
有一个整型数组 arr 和一个大小为 w 的窗口,从数组的最左边滑到最右边,窗口每次向右边滑一个位置。
例如,数组为[4,3,5,4,3,3,6,7],窗口大小为3时: [4 3 5]4 3 3 6 7; 4[3 5 4]3 3 6 7; 4 3[5 4 3]3 6 7; 4 3 5[4 3 3]6 7; 4 3 5 4[3 3 6]7; 4 3 5 4 3[3 6 7] ;
窗口中最大值为5;窗口中最大值为5; 窗口中最大值为5; 窗口中最大值为4; 窗口中最大值为6; 窗口中最大值为7。
如果数组长度为n,窗口大小为w,则一共产生n-w+1个窗口的最大值。 请实现一个函数。 输入:整型数组arr,窗口大小为w。 输出:一个长度为n-w+1的数组res,res[i]表示每一种窗口状态下的最大值。以本题为例,结果应该返回{5,5,5,4,6,7}。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<deque>
using namespace std;
//窗口中总有当前情况下最大的数
int* getMaxWindow(int arr[], int len, int w)//len为数组长度,w为窗口长度
{
if (arr == NULL || w < 1 || len < w)
return NULL;
deque<int> qmax;//双向列表(单调递减),存储下标
int* res = new int[len - w + 1];//存储最大值
int index = 0;
for (int i = 0; i < len; i++)
{
//如果i指向的数字比列表尾端的数字大,列表尾端的数字出列表
while (!qmax.empty() && arr[qmax.back()] <= arr[i])
qmax.pop_back();
//i指向的数字进列表,此时列表仍是单调递减
qmax.push_back(i);
//窗口扩张,最前面的出队列
if (qmax.front() == i - w)
qmax.pop_front();
//如果得到完整窗口,记录一个最大值
if (i >= w - 1)
res[index++] = arr[qmax.front()];
}
return res;
}
int main()
{
int arr[] = { 4,3,5,4,3,3,6,7 };
int* res = getMaxWindow(arr, 8, 3);
for (int i = 0; i < 6; i++)
cout << res[i] << " ";
return 0;
}
二、单调栈
2.1 定义
单调栈的目的是为了找到一个元素左边离它最近的且比它大的数及右边离它最近的且比它大的数。例如列表[8,5,4,6,7,],对于4来说,我们需要找到的是5和6;对于6来说,我们需要找到的是8和7。如果采用暴力方法,时间复杂度为,如果采用单调栈方法,时间复杂度为。
单调栈也需要维持一个单调递减或递增的结构,若要求离某个数最近且比它大的数,使用栈底到栈顶单调减的结构;反之则使用单调增的结构。例如[5,4,3,6,1],求每个元素左边离它最近的且比它大的数及右边离它最近的且比它大的数,使用单调减结构。首先5,4,3依次进栈,当6出现时,单调被打破,3出栈,同时3下面的数“4”为左边离它最近且比它大的数,导致3出栈的数“6”为右边离它最近且比它大的数;“4”出栈,分别输出5和6;“5”出栈,分别输出无(不存在左边比它大的数)和6;6进栈,1进栈,结束,进入清算阶段;“1”出栈,输出6和无;“6”出栈,输出无和无。
上面这种方式仅仅局限于数组内每个数字都只出现一次的情况,如果数字可重复出现,需要在栈的每个格子中放一条链表,用于存放多个相同数字的下标,其他逻辑不变。
每个数据仅进栈一次,出栈一次。 时间复杂度为
2.2 例题:指标A问题
定义:数组中累积和与最小值的乘积,假设叫做指标A。
给定一个数组,请返回子数组中,指标A最大的值。
这道题我们可以这样规定,在遍历数组的时候,遍历到每个元素的时候就将当前元素当作最小值组成的子数组然后求指标。
那我们可以将单调栈应用到这个场景当中,我每次求的当前元素的左边比自己小的且离自己最近的数的索引记录一下,然后将右边比自己小的且离自己最近的数的索引记录一下,那么在这两个索引之间组成的子数组的就是以当前元素为最小值。那么我们对每个数组对应的这样的子数组都求一下指标,那么整体返回数组的指标的最大值肯定在这其中。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<stack>
using namespace std;
int getMax(int n, int arr[])
{
int* sums = new int[n + 1];
sums[0] = arr[0];
//计算前缀和
for (int i = 1; i < n; i++)
sums[i] = sums[i - 1] + arr[i];
int Max = -999;
stack<int> mystack;//栈底的数小于栈顶的数,存储下标
for (int i = 0; i < n; i++)
{
//如果栈不为空且栈顶元素大于i当前指向的数组元素
while (!mystack.empty() && arr[mystack.top()] >= arr[i])
{
int j = mystack.top();
mystack.pop();
//如果栈是空的说明左边没有比它小的数,而此时进来的数比它小,所以该数最小的子数组的值即为sums数组中[i-1]位置上对应的值*arr[j],否则使用(sums[i-1]-sums[stack.top()])*arr[j]
Max = max(Max, (mystack.empty() ? sums[i - 1] : (sums[i - 1] - sums[mystack.top()])) * arr[j]);
}
mystack.push(i);
}
//清算数组中剩余的数
while (!mystack.empty())
{
int j = mystack.top();
mystack.pop();
//此时在栈中的数右边都没有比它小的数,如果栈为空说明它是数组中最小的数;栈不为空说明左边有比它小的数
//也就是当数组最后一个数最小时,以最小这个数单独成组的指标A的大小
Max = max(Max, (mystack.empty() ? sums[n - 1] : (sums[n - 1] - sums[mystack.top()])) * arr[j]);
}
return Max;
}
int main()
{
int dp[] = { 13,21,78,80,9 };
cout << getMax(5, dp);
}