窗口
窗口的数据结构使用的是单调双向队列。滑动时,加数只能从窗口右端(尾部)加,滑出数时数只能从左边(头部)滑出。注意:队列中存的是数组的下标。
以求最大值为例,流程为:
- 窗口进数时有两种情况:
- 新加的数比队列中最后一个数小,则直接在尾部加入队列;
- 新加的数比队列中最后一个数大或等于,则从尾部弹出旧数直到队列尾部的数严格大于新加入的数,再把新数加入队列尾部。注意:队列中的值要严格递减,若碰到相同的值则把旧值弹出放入新值的下标。
- 窗口滑出数时:若窗口滑动时,滑出的数是队列第一个数(最大值),则把最大值从头部弹出;否则说明队列中的最大值还没过期(还没从窗口滑出)。
举例与应用
题目一:固定窗口大小的题
贴代码:
public static int[] getMaxWindow(int[] arr, int w) {
if (arr.length <= 0 || w <= 0 || arr.length < w) return null;
int[] ans = new int[arr.length - w + 1];
LinkedList<Integer> queue = new LinkedList<>();
for (int i = 0; i < arr.length; i++) {
//加数时
while (!queue.isEmpty() && arr[queue.peekLast()] <= arr[i]) {
queue.pollLast();
}
queue.add(i);
//滑出数时
if (queue.peekFirst() == i - w) {
queue.pollFirst();
}
if (i >= w - 1) {
ans[i - w + 1] = arr[queue.peekFirst()];
}
}
return ans;
}
时间复杂度O(n)。
题目二:窗口大小变化的题
方法一:暴力解,时间复杂度O(n^3)。(两层循环+判断,不贴代码了)
方法二:滑动窗口法。
先提两点:
- 如果某一段子数组L~R是达标的,则此子数组内任意的子数组都达标;
- 如果某一子数组L~R已经不达标了,则包含此子数组的任意数组都不达标(即将此子数组往外扩形成的数组都不达标)。
先准备一个答案变量res,再准备一个保存滑动窗口最大值的单调双端队列qMax和一个保存滑动窗口最小值的单调双端对列qMin,一个L的指针表示窗口的左端,一个R的指针表示窗口的右端。下面是程序的流程:
- L和R先从角标0开始,若满足条件,R往右走,直到不满足条件,此时表示的是以L下标开始的最大窗口。令res += R - L,表示满足条件的子数组中以L开头的子数组的个数;(这里用到是上面的第一点:达标子数组中的任意子数组都达标)
- 由于已经不满足条件了,则L向左滑动一格,再次判断R能否向右滑动,重复1的步骤。(这里用到的是上面的第二点:不达标子数组再往外扩仍是不达标的,所以不要直接把R往右扩了,应该先把L向右滑动后再判断R是否能往右扩)
可以看到,L和R都一直往右走不回头,时间复杂度为O(n)。
贴代码:
public static int getMax(int[] arr, int num) {
int res = 0;
if (arr.length == 0) return res;
LinkedList<Integer> qMax = new LinkedList<>();
LinkedList<Integer> qMin = new LinkedList<>();
int L = 0;
int R = 0;
while (L < arr.length) {
while (R < arr.length) {
while (!qMax.isEmpty() && arr[qMax.peekLast()] <= arr[R]) {
qMax.pollLast();
}
qMax.addLast(R);
while (!qMin.isEmpty() && arr[qMin.peekLast()] >= arr[R]) {
qMin.pollLast();
}
qMin.addLast(R);
if (arr[qMax.peekFirst()] - arr[qMin.peekFirst()] > num) {
break;//不满足条件跳出
}
R++;
}
res += R - L;
if (qMax.peekFirst() == L) qMax.pollFirst();
if (qMin.peekFirst() == L) qMin.pollFirst();
L++;
}
return res;
}