【题目】
给定数组
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
)
的解法。
【解题思路】
本体最关键的地方是在于如果下手去遍历正数组拿到所有的子数组,再求子数组的最大值和最小值,确定子数组是否满足条件。
关键点一:子数组
数组[4, 3, 5, 4, 2, 1, 6, 7]
子数组有
[4] [4, 3] [4, 3, 5] [4, 3, 5, 4] [4, 3, 5, 4, 2] [4, 3, 5, 4, 2, 1] [4, 3, 5, 4, 2, 1, 6] [4, 3, 5, 4, 2, 1, 6, 7]
[3] [3, 5] [3, 5, 4] [3, 5, 4, 2] [3, 5, 4, 2, 1] [3, 5, 4, 2, 1, 6] [3, 5, 4, 2, 1, 6, 7]
[5] [5, 4] [5, 4, 2] [5, 4, 2, 1] [5, 4, 2, 1, 6] [5, 4, 2, 1, 6, 7]
[4] [4, 2] [4, 2, 1] [4, 2, 1, 6] [4, 2, 1, 6, 7]
[2] [2, 1] [2, 1, 6] [2, 1, 6, 7]
[1] [1, 6] [1, 6, 7]
[6] [6, 7]
[7]
子数组有
[4] [4, 3] [4, 3, 5] [4, 3, 5, 4] [4, 3, 5, 4, 2] [4, 3, 5, 4, 2, 1] [4, 3, 5, 4, 2, 1, 6] [4, 3, 5, 4, 2, 1, 6, 7]
[3] [3, 5] [3, 5, 4] [3, 5, 4, 2] [3, 5, 4, 2, 1] [3, 5, 4, 2, 1, 6] [3, 5, 4, 2, 1, 6, 7]
[5] [5, 4] [5, 4, 2] [5, 4, 2, 1] [5, 4, 2, 1, 6] [5, 4, 2, 1, 6, 7]
[4] [4, 2] [4, 2, 1] [4, 2, 1, 6] [4, 2, 1, 6, 7]
[2] [2, 1] [2, 1, 6] [2, 1, 6, 7]
[1] [1, 6] [1, 6, 7]
[6] [6, 7]
[7]
关键点二:如何遍历数组找到所有的子数组
如上所示,我们可以分别一数组的元素作为子元素的头元素,例如以 4 为首的子数组,有[4] [4, 3] [4, 3, 5].
.....
如果你能想到这种遍历的方式,恭喜你,这道题你已经答对了一半了。
这里解释下为什么这样遍历很方便。
我们可以设置两个变量 L 和 R,分别表示子数组的头部索引和尾部索引。刚开始遍历时L R都为0,L不动,R右移一位,如果子数组最大值 最小值满足条件,R继续右移,直到R右移到某一位置时,子数组最大值最小值不满足条件,R之后所有元素都将不满足条件。我们假设此时 L= i R = j
设此时子数组为 { arr[i], arr[i+1], arr[i+2], arr[i+3] ...... arr[j] } 最大值为max,最小值为min
R继续右移 子数组变为 { arr[i], arr[i+1], arr[i+2], arr[i+3] ...... arr[j],arr[j +1 ] } 因为R继续右移实在原来子数组的基础上扩大的,所以新的子数组 { arr[i], arr[i+1], arr[i+2], arr[i+3] ...... arr[j],arr[j +1 ] } 的最大值一定
≥ max ,而最小值一定
≤ min。
此时满足条件的子数组个数为 R - L
这时候我们需要右移 L,以新的数组元素 arr[i + 1] 作为子数组的头部元素,变量R 不需要回退到和L 重回的位置,因为新的子数组
{arr[i+1] }
{arr[i+1], arr[i+2] }
{arr[i+1], arr[i+2], arr[i+3] }
......
{arr[i+1], arr[i+2], arr[i+3] ...... arr[j] }
{arr[i+1], arr[i+2], arr[i+3] ...... arr[j] }
这些子数组都是满足条件的,因为这些子数组的最大值一定
≤ max ,而最小值一定
≥ min
移动 L 后 R 可以在原来的基础上继续后移,因为R没有后移
,所以我们整体遍历数组的时间复杂度为O(N)(这里假设求最大 最小值为时间复杂度为O(1) ) 下一次遍历开始的子数组为 {arr[i+1], arr[i+2], arr[i+3] ...... arr[j] ,arr[j + 1]}
上面的这句话标红的这句话的让我想到了 前段时间刚写过的KMP算法,有异曲同工之妙!!!
![](https://img-blog.csdnimg.cn/20200728002929342.png)
这样我们就得到了两个很实用的结论:
- 如果子数组 arr[i..j]满足条件,即 max(arr[i..j])-min(arr[i..j])<=num,那么 arr[i..j]中的每一 个子数组,即 arr[k..l](i≤k≤l≤j)都满足条件。我们以子数组 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]的子数组,即 arr[k..l](k≤i≤j≤l)都不满足条件。证明过程同第一个结论
关键点三:找到子数组的最大值,最小值
在O(1)时间复杂度了找到子数组的最大值和最小值,可以把每次遍历的子数组看作时一个滑动窗口,利用单调的双端队列可以在O(1)时间复杂度内找到最大值和最小值。关于在滑动窗口内找到最大值和最小值可以参考我的另一篇博客笔记,这里就不再赘述
至此我们已经基本的解题思路有了。
整体的流程如下(以下内容摘自左神的《程序员代码面试指南》)
- 生成两个双端队列 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[2]开头的子数组,满足条件的数量有多少个,全部累加起来就是答案。
上述过程中,所有的下标值最多进 qmax 和 qmin 一次,出 qmax 和 qmin 一次。i 和 j 的值 也不断增加,并且从来不减小。所以整个过程的时间复杂度为 O(N)。
【代码如下】
public static int getCountOfMaxSubMinLessThanNum(int[] arr, int sum) {
int count = 0;// 满足条件的子数组个数
// 双端队列维护子数组(滑动窗口)最大值,队列内数据依次递减 队头为最大值
LinkedList<Integer> maxQue = new LinkedList<>();
// 双端队列维护子数组(滑动窗口)最小值,队列内数据依次递增 队头为最大值
LinkedList<Integer> minQue = new LinkedList<>();
int i = 0, j = 0;
while (i < arr.length) {
while (j < arr.length) {
while (!maxQue.isEmpty() && arr[i] >= arr[maxQue.getFirst()]) {
maxQue.removeLast();
}
maxQue.addLast(i);// 加入到队列尾部
while (!minQue.isEmpty() && arr[i] <= arr[minQue.getFirst()]) {
minQue.removeLast();
}
minQue.addLast(i);// 加入到队列尾部
if (arr[maxQue.getFirst()] - arr[minQue.getFirst()] <= sum) {
j++;
} else {
j++;
break;
}
}
count += j - i;
// 这里需要判断最大值是否为队列的首个元素,如果不移出,有可能之后的所有元素都没有arr[i]大,
//但是显然之后在遍历的新的子数组已经以arr[i+1]为首,已经不再包含rr[i]元素了,所以要移除arr[i] 同理最小值。
if (minQue.peekFirst() == i) {
minQue.pollFirst();
}
if (maxQue.peekFirst() == i) {
maxQue.pollFirst();
}
i++;
}
return count;
}
后序:
自己写博客纯粹时为了整理下自己的学习笔记以便后续复习使用,各位算法大佬不喜勿喷。
还不太会弄代码片段的对齐格式。