一、窗口数组最大值原型:
描述:窗口大小为w,数组长度为n,求每个窗口数组的最大值,如下题返回一个数组为 [5 5 5 4 6 7]
流程: 准备一个双端队列,存放窗口数组最大值的下标准,双端队列保证从左往右递增,不包括等于,
1)窗口加数(R往右边移动):考察双端队列尾部和数组当前数的关系,尾部数大,直接从队尾入队,否则弹出,直到遇到尾部大于当前数的才将该数从队尾入队。
2)窗口减数(L往有边移动):考察双端队列的首部元素是否过期,过期则弹出即可,否则不弹出。
这里保证双端队列从大到小,相等也不行,因为相等不跟新的话最大值可能是前一个窗口留下来的。
前面为了方便理解在双端队列中存放的数窗口的最大值,最后的方法就是存放的数原数组的下标,这样既可可以通过索引得到值,又可以得到该值在原数组的位置。
实现代码:
#include<iostream>
#include<vector>
#include<deque>
using namespace std;
vector<int> getMaxValueWindow(int arr[], int arr_len, int w) {
vector<int> res;
if (arr == NULL || w < 1 || arr_len < w) {
return res;
}
deque<int> dq;
int L = 0;//窗口左边界
int R = 0;//窗口右边界
for (int i = 0; i < arr_len; ++i) {
while (!dq.empty() && arr[dq.back()] <= arr[i]) {//只要队列不为空,并且比进来的数要比队尾的小,则弹出
dq.pop_back();
}
dq.push_back(i);
if (R - L + 1 != w)//还没到窗口,右边R移动
R++;
else {//到了窗口
res.push_back(dq.front());//取出第一个窗口最大数组放入数组中
if (dq.front() == L) {//判断窗口左边界是否到期,到期直接弹出
dq.pop_front();
}
L++;
R++;
}
}
return res;
}
int main() {
int arr[] = { 5,4,1,2,6,0,5,1,6,9,10,23 };
int w;
while (cin >> w) {
vector<int> res = getMaxValueWindow(arr, 12, w);
for (int i = 0; i < res.size(); ++i) {
cout << arr[res[i]] << ' ';
}
cout << endl;
}
}
二、应用:
方法一:暴力解,两个for循环搞出所有子数组,然后用一个函数判断该子数组是否满足情况即可:
1)for循环遍历,得到子数组的下标:
int getNum1(int arr[], int arr_len, int num) {
int res = 0;
for (int start = 0; start < arr_len; start++) {//通过两个for循环搞出所有子数组,然后用一个isValid函数判断数组是否有效
for (int end = start; end < arr_len; end++) {
if (isValid(arr, start, end, num)) {
res++;
}
}
}
return res;
}
2)isVlid函数判断子数组是否满足条件:
bool isValid(int arr[],int start, int end,int num) {
int MAX = INT_MIN;
int MIN = INT_MAX;
for (int i = start; i < end; ++i) {//求出子数组的最大值和最小值
MAX = MAX > arr[i] ? MAX : arr[i];
MIN = MIN < arr[i] ? MIN : arr[i];
}
return MAX - MIN <= num;
}
上述那个for两个循环O(N^2)然后isValid又是O(N),所以是O(N^3)...
方法二:O(N)的时间复杂度
思路:
1)如果一个窗口数组满足max-min<=num,则其内部所有子数组都满足max-min<=num。因为其子数组中的sub_max
只能比max小,sub_min只能比min大,之差所以一定满足<=num.
2)如果一个窗口数组满足max-min<=num,再增加一个元素就不达标了,那么窗口无论继续增加多少元素都不达标,因为继续扩只能让max变大min变小的可能性增加。
3)理解了1和2思路就会变得异常简单。让L停留在0,R从0往右边扩,直到遇到一个不达标的停止,则该窗口内所有子数组都达标,统计达标的子数组个数;然后L前进一步,窗口内有可能就会达标了,所以此时让R继续扩,就有L每前进一步R就往右走。这样准备两个双端队列,一个存放窗口最大数组,一个存放窗口最小数组即可。
测试代码:
#include<iostream>
#include<vector>
#include<deque>
using namespace std;
bool isValid(int arr[], int start, int end, int num);
//暴力搜索
int getNum1(int arr[], int arr_len, int num) {
int res = 0;
for (int start = 0; start < arr_len; start++) {//通过两个for循环搞出所有子数组,然后用一个isValid函数判断数组是否有效
for (int end = start; end < arr_len; end++) {
if (isValid(arr, start, end, num)) {
res++;
}
}
}
return res;
}
bool isValid(int arr[], int start, int end, int num) {
int MAX = INT_MIN;
int MIN = INT_MAX;
for (int i = start; i <= end; ++i) {
MAX = MAX > arr[i] ? MAX : arr[i];
MIN = MIN < arr[i] ? MIN : arr[i];
}
return MAX - MIN <= num;
}
//窗口数组最大值方法
int getNum2(int arr[], int arr_len,int num) {
int res = 0;
if (arr == NULL) {
return res;
}
deque<int> dq_max;
deque<int> dq_min;
int L = 0;//窗口左边界
int R = 0;//窗口右边界
while (L < arr_len) {
while (R < arr_len) {
//窗口数组的最大值
while (!dq_max.empty() && arr[dq_max.back()] <= arr[R]) {
dq_max.pop_back();
}
dq_max.push_back(R);
//窗口数组的最小值
while (!dq_min.empty() && arr[dq_min.back()] >= arr[R]) {
dq_min.pop_back();
}
dq_min.push_back(R);
if (arr[dq_max.front()] - arr[dq_min.front()] > num) {//不达标直接break,后面的都不用考虑了
break;//如果不到R小于arr_len berak之后还会进来该循环
}
R++;
}
if (dq_max.front() == L) {//判断窗口左边界是否到期,到期直接弹出,然后L++
dq_max.pop_front();
}
if (dq_min.front() == L) {//判断窗口左边界是否到期,到期直接弹出,然后L++
dq_min.pop_front();
}
res += R - L;//这个边界需要注意,是R-L而不是R-L+1是因为R在循环中已经到下一个位置了。
//如:0-3本来有子数组:0-0,0-1,0-2,0-3 就是3-0+1=4个,但是现在在while中R已经来到了4的位置,所以就是直接R-L.
L++;
}
return res;
}
int main() {
int arr[] = {5,4,7,8,6,3,1,10,9,11};
int w;
while (cin >> w) {
int res1 = getNum1(arr, 10, w);
cout << res1 << endl;
int res2 = getNum2(arr, 10, w);
cout <<res2 << endl;
}
}