题目
给定一个大小为 n≤10^6≤10^6 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7]
,k为 3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 n和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
思路1
一个简单的思路,就是在每一个滑动窗口中,遍历一次滑动窗口中的数,找到滑动窗口中的最大值和最小值,并用两个数组把它们存起来。若数组长度为n,滑动窗口的长度k,则有n-k+1个滑动窗口,每个窗口遍历k个数,时间复杂度O(nk)。
那有什么可以减少比较的次数吗?我们发现每次滑动窗口时,只有一个数发生了改变,即原滑动窗口左端元素被推出,而窗口右端进入一个新元素。如果标记原窗口最大最小值及其位置,那么在新的窗口中,若最大最小值仍在新窗口中,则只需要拿新元素和原来的最大最小值比较并更新,若最大或最小值不在新窗口中,我们再遍历一次窗口。时间复杂度在O(n)~O(nk)之间。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 +10;
typedef pair<int, int> PII;
int ma[N], a[N];//a[]存储输入数组,ma[]存每个窗口最大值
PII lmi, lma;//临时保存的最大最小值及其位置
//判断最大最小值是否仍在窗口
bool check_out(int st) {
if (st > lmi.second || st > lma.second) return true;//不在时
return false;
}
//查找最大最小值
void refind(int st, int en) {
for (int i = st; i <= en; i++) {
if (lmi.first >= a[i]) {
lmi.first = a[i];
lmi.second = i;
}
if (lma.first <= a[i]) {
lma.first = a[i];
lma.second = i;
}
}
}
int main () {
lmi.first = INT_MAX, lmi.second = 0,lma.first = INT_MIN, lma.second = 0;
int n, k;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
int st = 1, en = k;
while (st <= (n - k + 1) && en <= n) {
while (check_out(st) && st <= (n - k + 1) && en <= n) {//最大或最小值不在窗口
lmi.first = INT_MAX,lma.first = INT_MIN;
refind(st, en);
printf("%d ",lmi.first);
ma[st] = lma.first;
st++, en++;
}
while (!check_out(st) && st <= (n - k + 1) && en <= n) {
refind(en ,en);
printf("%d ",lmi.first);
ma[st] = lma.first;
st++, en++;
}
}
printf("\n");
for (int i = 1; i < st; i++) {
printf("%d ", ma[i]);
}
return 0;
}
思路2
从上面思路1中,上一个窗口的最大或最小值不在新窗口中时,我们依旧遍历一次窗口。原因是我们并不知道第二大或第二小的值在什么地方。
下面我以找最小值为例。
我们能否再定义一个双端队列,里面保存窗口中元素的下标,下标的排序具有这个特点:下标对应的数组元素从小到大排序。比如样例中的窗口[1,3,-1],取出下标后在队列中的排序是下面这样的。
我们为什么要这样子做呢?当我们想要窗口最值时,可以从队头中获得。而当最小值不在新窗口中时,即队列中第一个元素对应的数组元素不在新窗口时,我们把队头元素推出,这时新的队头对应的数组元素再先新元素比较,如果新元素不是最小值,就把它的下标放到队列的适当位置。
但貌似插入操作也是挺费时的,我们能否找到一个性质,使得队列元素对应的数组元素从小到大排序,并且不需要插入呢?我们创建队列的目的,就是当上一个窗口的最小值不在新窗口中时,减少查找新的最小值的次数。我们发现,当新元素比原来窗口的一些元素要小时,那么这些元素就是没有用的元素,因为它们的位置在新元素之前,会比新元素提前被推出滑动窗口,同时由于新元素比这些元素要小,这些元素永远不会被当做最小值输出。这一规律反映到队列中,就是把这些数组元素的下标从队列推出删除。
那我们怎么知道什么时候该输出队头元素对应在数组中的元素呢?由于队列元素为滑动窗口经过的前面的元素中选择,并且队头永远是滑动窗口最小的,每次当窗口要滑动时,我们就可以输出。
怎么判断窗口滑动呢?用for 循环遍历一遍数组a[],当i >= k - 1(i从 0 开始)时每次输出队头元素。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int a[N];
//int hh = 0, ee = -1;, q[N]
deque<int> q;
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
for (int i = 0; i < n; i++) {
if (!q.empty() && q.front() < i - k + 1)q.pop_front();
while(!q.empty() && a[q.back()] >= a[i]) q.pop_back();
q.push_back(i);
if (i >= k - 1) printf("%d ", a[q.front()]);
}
puts("");
q.clear();
for (int i = 0; i < n; i++) {
if (!q.empty() && q.front() < i - k + 1)q.pop_front();
while(!q.empty() && a[q.back()] <= a[i]) q.pop_back();
q.push_back(i);
if (i >= k - 1) printf("%d ", a[q.front()]);
}
return 0;
}
自定义双端队列,速度更快。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 +10;
int a[N], q[N];
int hh = 0, tt = -1;
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
for (int i = 0; i < n; i++) {
if (hh <= tt && q[hh] < i - k + 1) hh++;//判断最小值是否还在滑动窗口中,不在就推出元素
while (hh <= tt && a[q[tt]] >= a[i]) tt--;//当队尾元素对应a[]中的元素比滑动窗口的最后一个元素大,推出
q[++tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
puts("");
hh = 0, tt = -1;
for (int i = 0; i < n; i++) {
if (hh <= tt && q[hh] < i - k + 1) hh++;//判断最小值是否还在滑动窗口中
while (hh <= tt && a[q[tt]] <= a[i]) tt--;//队头放最大的元素下标
q[++tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
return 0;
}