第一章 栈和队列
1.10 最大值减去最小值小于或等于 num 的子数组数量
【题目】
给定数组 arr 和整数 num,共返回右多少个子数组满足如下情况:
- max(arr[i…j)-min(arr[i…j) ≤ num
- max(arr[i…j) 表示子数组 arr[i…j] 中的最大值,min(arr[i…j) 表示子数组 arr[i…j] 中的最小值。
【要求】
如果数组长度为 N,请实现时间复杂度为 O(N) 的解法。
【难度】
校 ★★★☆
【题解】
最普通的解法:找到 arr 的所有子数组,一共 O(N) 个,然后对每一个子数组做遍历找到其中的最小值和最大值,这个过程的时间复杂度为 O(N),然后判读这个子数组是否符合条件,统计所有满足的子数组数量即可。整体的时间复杂度为 O(N),显然不符合题意。
最优解可以实现时间复杂度为 O(N),额外空间复杂度 O(N),这需要借助双端队列和滑动窗口结构完成。首先,生成两个双端队列 qMax 和 qMin。当子数组为 arr[i…j] 时,qMax 维护了子窗口数组 arr[i…j] 的最大值更新的结构,qMin 维护了窗口子数组 arr[i…j] 的最小值更新的结构。当子数组 arr[i…j] 向右扩充一个位置变成 arr[i…j+1] 时,qMax 和 aMin 结构可以在 O(1) 的时间内更新,并且可以在 O(1) 的时间内得到 arr[i…j+1] 的最大值和最小值。当子数组 arr[i…j] 向左缩小一个位置变为 arr[i+1…j],qMax 和 aMin 结构依然可以在 O(1) 的时间内更新,并且在 O(1) 的时间内得到 arr[i+1…j] 的最大值和最小值。
通过上述分析,可以得到以下两个结论:
- 如果子数组 arr[i…j] 满足条件,即 max(arr[i…j)-min(arr[i…j) ≤ num,那么 arr[i…j] 中的每一个子数组,即 arrk…m 都满足条件。以子数组 arr[i…j-1] 为例说明,arr[i…j-1] 最大值只可能小于或等于 arr[i…j] 的最大值,arr[i…j-1] 的最小值只可能大于或等于 arr[i…j] 的最小值,所以 arr[i…j-1] 必然满足条件。同理,arr[i…j] 中的每个子数组都满足条件。
- 如果子数组 arr[i…j] 不满足条件,那么所有包含 arr[i…j] 的子数组,即 arrk…m 都不满足条件。证明同第一个结论。
根据双端队列 qMax 和 qMin 的结构性质,以及如上两个结论,设计如下:
- 生成两个双端队列 qMax 和 qMin。生成两个整形变量 i 和 j,表示子子数组的范围,即 arr[i…j]。生成整形变量 res,表示所有满足条件的子数组数量。
- 令 j 不断向右移动(j++),表示 arr[i…j] 一直向右扩大,并不断更新 qMax 和 qMin结构,保证 qMax 和 qMin 始终维持动态窗口最大值和最小值的更新结构。一旦出现 arr[i…j] 不满足条件的情况,j 向右扩的过程停止,此时 arr[i…j-1]、arr[i…j-2]、arr[i…j-3]、…、arr[i…i] 一定是满足条件的。也就是说,所有必须以 arr[i] 作为第一个元素的子数组,满足条件的数量为 j-i 个,于是 res+=(j-i)。
- 当进行完步骤 2,令 i 向右移动一个位置,并对 qMax 和 qMin 做相应的更新,qMax 和 qMin 从原来的 arr[i…j] 窗口变为 arr[i+1…j] 窗口的最大值和最小值的更新结构。然后重复步骤 2,求出以 arr[i+1] 作为第一个元素的子数组中满足条件的数量。
- 根据步骤 2 和步骤 3,依次求出以 arr[0]、arr[1]、…、arr[N-1] 作为第一个元素的子数组中满足条件的数量,累加起来就是最终的结果。
上述过程中,所有的下标值最多进出 qMax 或 qMin 一次,所以整个过程的时间复杂度为 O(N)。
【实现】
- MaxMinusMinLENum.java
import java.util.Deque;
import java.util.LinkedList;
/**
* 最大值减最小值小于或等于 num 的子数组数量
*/
public class MaxMinusMinLENum {
private int[] arr;
private int num;
private int count;
public MaxMinusMinLENum(int[] arr, int num) {
this.arr = arr;
this.num = num;
this.count = -1;
}
public void setArr(int[] arr) {
this.arr = arr;
process();
}
public void setNum(int num) {
this.num = num;
process();
}
public int getCount() {
if (count < 0) {
process();
}
return this.count;
}
private void process() {
/**
* 特殊情况:
*/
if (this.arr == null || this.arr.length == 0 || this.num < 0) {
this.count = 0;
return;
}
this.count = 0;
/**
* 双端队列
*/
Deque<Integer> qMax = new LinkedList<>();
Deque<Integer> qMin = new LinkedList<>();
/**
* 动态窗口左边界 i
*/
for (int i = 0, j = 0; i < this.arr.length; ++i) {
/**
* 动态窗口右边界 j
*/
for (; j < this.arr.length; ++j) {
/**
*
*/
while (!qMax.isEmpty() && this.arr[qMax.peekLast()] <= this.arr[j]) {
qMax.pollLast();
}
qMax.addLast(j);
/**
*
*/
while (!qMin.isEmpty() && this.arr[qMin.peekLast()] >= this.arr[j]) {
qMin.pollLast();
}
qMin.addLast(j);
/**
* 窗口数组不满足条件
*/
if (this.arr[qMax.getFirst()] - this.arr[qMin.getFirst()] > this.num) {
break;
}
}
if (qMax.peekFirst() == i) {
qMax.pollLast();
}
if (qMin.peekFirst() == i) {
qMin.pollFirst();
}
this.count += j - i;
}
}
}
- MaxMinusMinLENumTest.java
public class MaxMinusMinLENumTest {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
int num = 3;
MaxMinusMinLENum instance = new MaxMinusMinLENum(arr, num);
int count = instance.getCount();
System.out.println(count);
}
}