问题阐述
窗口:可以理解为一段连续子数组,下标范围为(L,R]
滑动窗口: L、R会改变,改变方式是右移,R会右移,L也会右移,但始终保持L<R
滑动窗口的最大值/最小值更新结构:对某一个数组,初始时,L、R均位于最左端,L、R只可以向右移,且始终有L<R,在L与R向右移动的过程中,该结构能始终维护滑动窗口内的最大值(或最小值),也就是说,通过该结构,可以迅速获得当前滑动窗口的最大值(或最小值)
流程
以数组arr[] = {9,3,2,1,6,7,7,10}为例解释滑动窗口最大值更新结构维护的过程,使用【】来表示滑动窗口的范围
该更新结构为双端队列,双端结构中存放数组的下标,且存放时保证按顺序存放的下标中的数递减
具体流程如下所示:
- 【】9 3 10 1 7 7
此时滑动窗口中没有值,双端队列为空
R右移
2. 【9】3 10 1 7 7
R右移一位,此时滑动窗口出现了一个数为9,于是将9的下标从队尾入队
此时的双端队列为 [ 0->9 ] (为方便观察,将下标指向的数也写在这里)
3. 【9 3】 10 1 7 7
R右移一位,3进入滑动窗口,3小于队尾0位置处的9,将3的下标从队尾入队
此时的双端队列为 [ 0->9 , 1->3 ]
4. 【9 3 10】 1 7 7
R右移一位,10进入滑动窗口,10大于队尾1位置处的3,将队尾出队
再比较发现10仍然大于队尾0位置处的9,将队尾出队,此时,双端队列为空了,将10的下标入队
此时的双端队列为 [ 2->10 ]
5. 【9 3 10 1】 7 7
R右移一位,1进入滑动窗口,此时的情况与步骤3相似,直接将1的下标入队
此时的双端队列为 [ 2->10 ,3->1 ]
6. 【9 3 10 1 7】 7
R右移一位,7进入滑动窗口,7大于队尾3位置处的1,将队尾出队
再比较发现7小于队尾2位置处的10,将7的下标入队
此时的双端队列为 [ 2->10 ,4->7 ]
L右移
7. 9【 3 10 1 7】 7
将当前移出滑动窗口数字的下标(过期的下标)与双端队列队首的下标比较,如果过期的下标与队首下标相等,则将队首出队,因为队首已经不在滑动窗口内了,否则,不做任何处理
此时的双端队列仍然为 [ 2->10 ,4->7 ]
8. 9 3 【10 1 7】 7
此时的双端队列仍然为 [ 2->10 ,4->7 ]
9. 9 3 10 【 1 7】 7
L右移一位后下标2过期,将队首出队
此时的双端队列为 [ 4->7 ]
最大值
当前滑动窗口的最大值就是双端队列的队首下标数据
将双端队列中的数据理解为:在当前滑动窗口下,该下标处数据为最大值的可能性
由于L、R都只能右移,所以,如果当前新加入滑动窗口的数据比队尾元素大,那么队尾元素不可能再成为滑动窗口的最大值,如果当前新加入的滑动窗口的数据比队尾元素小,那么当前新加入的元素可能在队尾元素过期后成为滑动窗口的最大值。
因此,我们可以考虑,当当前新加入滑动窗口的数据与队尾元素相等,应该如何操作?
答案是将队尾元素出队,然后将当前元素的下标入队,因为当前元素一定比队尾元素晚过期
10. 9 3 10 【 1 7 7 】
此时的双端队列然为 [ 4->7 ]
要得到滑动窗口最小值更新结构,可以将双端队列中存放数据的规则改为递增排列
例题
1)给定一个数组num,对数组中长度为m的所有连续子数组,存在一个最大值,求这些最大值中的最小值
这是今年秋招时,我遇到的华为第一轮面试的手撕代码题,现在回头看,发现是一个典型的滑动窗口问题。
思路:构造一个初始长度为m的滑动窗口更新结构,将L和R同时右移,可以很容易得到所有的长度为m的连续子数组以及这些连续子数组的最大值
int FindMinOfMax(vector<int> &v, int m)
{
if (v.size() < m) return -1;
deque<int> maxq; //窗口最大值结构
int L = -1, R = -1;
int ret = 0xffffff7; //将ret设定为一个比较大的数
while (R+1< v.size())
{
while (R - L < m && R+1<v.size()) //如果当前滑动窗口长度小于m,且R不会移出界,R右移
{
R++;
while (!maxq.empty() && v[maxq.back()] <= v[R])
{
maxq.pop_back();
}
maxq.push_back(R);
}
//while循环跳出有两种可能:
//1 滑动窗口长度等于m,得到当前滑动窗口的最大值,更新ret值,然后将L右移
//2 R再右移要出界了,那么大while循环也到此结束
if (R - L == m)
{
ret = min(ret, v[maxq.front()]);
L++;
if (maxq.front() <= L) maxq.pop_front();
}
}
return ret;
}
2)给出长度为M的数组中,求出满足最大值-最小值<n所有连续子数组
思路:如果某连续子数组满足该指标(最大值-最小值<n),则其子集也一定满足
L,R首先都位于-1位置,R先右移,只要此时的滑动窗口满足条件,R就一直右移,直到滑动窗口不满足条件时,以L为首,末尾下标不超过R-1的窗口均能满足条件,全部输出,然后让L右移,判断此时滑动窗口是否满足条件,如果满足条件,R继续右移,否则,以L为首,末尾下标不超过R-1的窗口均能满足条件,全部输出。重复该过程,直到R到达数组的末尾。
//给出长度为M的数组中,求出满足最大值啊-最小值<n所有连续子数组
//返回值只返回满足条件的连续子数组的头尾下标
//思路:如果某连续子数组满足该指标,则其子集也一定满足
vector<vector<int>> fun(vector<int> &v, int n)
{
vector<vector<int>> retvv;
deque<int> mqueue1; //最大值更新结构
deque<int> mqueue2; //最小值更新结构
int L = -1, R = -1;
int maxValue;
int minValue;
while (R==-1 || L<R)
{
if (R + 1 < v.size()) //如果R未到达末尾,R右移
{
R++;
while (!mqueue1.empty() && v[mqueue1.back()] <= v[R])
{
mqueue1.pop_back();
}
mqueue1.push_back(R);
while (!mqueue2.empty() && v[mqueue2.back()] >= v[R])
{
mqueue2.pop_back();
}
mqueue2.push_back(R);
}
else //R到达末尾,可以直接根据当前窗口情况输出所有结果
{
int end;
if (maxValue - minValue < n) end = R;
else end = R-1;
while (L < R)
{
for (int i = L + 1; i <= end; i++)
{
cout << L << "," << i << endl;
vector<int> tempv = { L,i };
retvv.push_back(tempv);
}
L++;
}
break;
}
maxValue = v[mqueue1.front()];
minValue = v[mqueue2.front()];
while (maxValue - minValue > n )
{
for (int i = L+1; i < R; i++)
{
cout << L << "," << i << endl;
vector<int> tempv = { L,i };
retvv.push_back(tempv);
}
L++;
if (L < R)
{
if (L == mqueue1.front()) mqueue1.pop_front();
if (L == mqueue2.front()) mqueue2.pop_front();
maxValue = v[mqueue1.front()];
minValue = v[mqueue2.front()];
}
else break;
}
}
return retvv;
}
如有疏漏,请指正。