求数组arr中达标的子数组的数量:子数组内最大值与最小值之差小于等于target
提示:经典的单调栈的应用题目
极度重要的基础知识点:单调栈:
(1)单调栈:o(1)复杂度求数组arr在窗口w内的最大值或者最小值
想要做出本题来,必须理解上面的单调栈基础知识(1)。
想要做出本题来,必须理解上面的单调栈基础知识(1)。
想要做出本题来,必须理解上面的单调栈基础知识(1)。
题目
给定数组arr,给定整数target,如果arr中任意子数组内最大值与最小值之差小于等于target,则称之为达标子数组,请你求arr中达标的子数组的数量。
一、审题
示例:arr=3 2 1 4
target=1
咱们举例完全可以用暴力解枚举,也启发咱们的宏观调度
ans=0收集结果;
(1)不妨设让3做开头,则看看以i=0开头的子数组,连续有几个达标的?
下图可见,3 可以达标,3 2可以打标,但是3 2 1不达标,所以往后以3开头的子数组全部不可能达标!,因为子数组连续的
ans +=2
(2)让i=1做开头,2 可以达标,2 1达标,2 14 不达标,往后也不可能达标了
ans = 2+2=4
(3)让i=2开头试试有几个?1 可以达标,1 4 不可能
ans = 4+1=5
(4)以i=3开头的有几个?4 可以达标
ans = 5+1=6
所以答案就6个!
暴力解:不可
上面示例中,咱们就用暴力解搞定了一下
以每一个i位置为头,看看它能往右使劲扩展多少个子数组出来?
累加起来就是总的数量
外围的宏观调度就需要o(n)
中间以i开头刷一遍子数组,需要o(n)
每一个子数组,找max和min,又需要o(n)才能判断是否达标?
这样的话暴力解的复杂度o(n^3)肯定不行的
理解单调栈如何将o(n^2)优化到o(1)的
看看暴力解就知道,外围的以每次L开头,要去求一个结果n,这个宏观调度是躲不了的,外围的**o(n)**就是必然的
结果就是ans=n1+n2+……+nn;
咱们想办法在求内部一个子数组达标这件事上,看看能不能把o(n^2)的复杂度,优化到o(n)乃至o(1)??
根据(1)单调栈:o(1)复杂度求数组arr在窗口w内的最大值或者最小值
咱们有可能找到突破口!
事情是这样的:我们不妨设窗口内子数组范围是[L–R),左闭右开,之前单调栈求过一个窗口内的max和min是o(1)速度就可以拿到
有了单调栈,就可以把内部的o(n^2)的复杂度,优化到o(n)
而以L开头,R不断往右扩,达标,就让R++,一旦不达标,R截止。
这个o(n)的扩充过程,可以优化到o(1)吗?【事实是可以!】
真的需要每一次,都从L那开始遍历找min和max去判断子数组L–R是否达标吗?
不需要!
本题的特点就在这,示例中咱们也看到了,当L开头的子数组,往右扩,只要在第一个R不达标的地方截止,往后R再扩,没用了,都不会达标
比如下图:L=0开头,L–R=0可以,L–R=1可以,L–R=2可以,L–R=3 不 可以,那从此往后,以L=0开头的子数组,R往后扩都不会达标!
那么刚刚扩到第一个R=3处不达标,L–R中有多少个达标呢?R-L个全部达标,
因为前L–R递增过程中,每一个子数组你都达标,咱们才扩R的。
第一个R=3不达标,从此往后不再扩,也不会达标的。
因此R实际上扩了几次达标呢?R=0,1,2,3次,恰好也是R-L。
好,现在L++,咱们看下一个L开头的情况:
从现在L开始,R真的需要回到L这个地方,从头去判断各个L–R都达标吗?
不需要!
不需要!
不需要!
为什么??因为上一个L–R达标的话,意味着内部任意子数组都达标的【max-min<=target】
如果L–R达标,则必然L+1–R也达标,如果L+1–R不满足这个条件,你这个R早就不能扩了。看下图:
图中L–R是达标的,那L+1–R必然达标
现在L=L+1了,你不需要把R整回到L重新判断一遍了,此时的L–R天然达标的,因为上一轮【L+1–R必然达标】给你做过了达标判断。
现在L=L+1时,你只需直接看当前的L–R是否达标即可!!!【上一轮R是不包括的】
!这是不是非常非常巧妙
!这就是单调栈带来的好处,具备单调性。R不用重新回来搞一遍,
所以中间L–R这个遍历子数组的o(n)的过程,咱们优化到o(1)了。
外围L从0开头,1开头……N-1开头这个o(n)的宏观调度避免不了。
内部的o(n^2)【找max和min与遍历L–R同时】被优化到o(1)
所以咱们用单调栈来解决这个问题,也就总体**o(n)**的速度!!这可比暴力解优化了太太多了吧!!!
单调栈解决本题的代码逻辑
OK,总结一下,咱们怎么做这个题,写代码呢?
利用L–R左闭右开,表示窗口内达标的子数组
准备好max和min的单调栈:双向队列,他们的求法代码都是同一套逻辑【看基础知识那个文章!】。
(0)外围宏观调度,让每一个L做一次开头,统计有每个L开头时,有多少个子数组达标?这个数量,累加到ans做结果
(1)内部呢,每一次L开头,L–R内用单调栈找max和min,o(1)速度拿到他们俩,判断max-min<=target吗?
是,继续扩R++,否则,R就是第一次性不达标的R,从此往后不必再扩R了,统计ans+=R-L
(2)(1)不能扩了,那换开头L再统计,L++;新的L开头,也不需要R再回到L重新刷一遍,只需要去(1)继续判断就行。
中间一定要注意,L–R作为一个窗口,窗长自然是不固定的,所以R++时,不需要检查L是否过期,咱们一定要收集L–R内的max和min
知道R第一个不达标,咱们再收集答案,判断L是否过期,如何判断L过期?是单调栈内部的重要知识【看基础知识那个文章!】
整体只剩外围o(n)复杂度,是不是超级超级牛?
手撕代码
//复习
public static int validNumOfArr(int[] arr, int target){
if (arr == null || arr.length == 0) return 0;
int N = arr.length;
//利用L--R左闭右开,表示窗口内达标的子数组
//准备好max和min的单调栈:双向队列,他们的求法代码都是同一套逻辑【看基础知识那个文章!】
int L = 0;//左闭
int R = 0;//右开
int ans = 0;//结果统计数量
LinkedList<Integer> queueMax = new LinkedList<>();
LinkedList<Integer> queueMin = new LinkedList<>();
// (0)**外围宏观调度**,让每一个L做一次开头,统计有每个L开头时,
// 有多少个子数组达标?这个数量,累加到ans做结果
while (L < N){
// (1)内部呢,每一次L开头,L--R内用单调栈找max和min,o(1)速度拿到他们俩,判断max-min<=target吗?
// 是,继续扩R++,否则,R就是第一次性不达标的R,从此往后不必再扩R了,统计ans+=R-L
while (R < N){//达标继续扩
//达标与否,需要拿到max和min,这需要单调栈调度
while (!queueMax.isEmpty() && arr[R] >= arr[queueMax.peekLast()])
queueMax.pollLast();//扩R,就要让R进队列,大逼小离开
//然后大的进来
queueMax.addLast(R);
while (!queueMin.isEmpty() && arr[R] <= arr[queueMin.peekLast()])
queueMin.pollLast();//扩R,就要让R进队列,小逼大离开--跟上面逻辑类似
//然后大的进来
queueMin.addLast(R);
//判断达标才能继续扩,否则结束
int max = arr[queueMax.peekFirst()];
int min = arr[queueMin.peekFirst()];
if (max - min > target) break;//不达标,结束R东扩
//一直到第一个R不能扩
R++;
}
//然后扩不动了就要统计结果!
ans += R - L;
//对于单调栈的调度,扩不动R自后就要核验队头是否过期?
//w不是固定长度哦,所以不是要在R扩时,检查窗长w,而R不能扩时再检查
if (L == queueMax.peekFirst()) queueMax.pollFirst();//弹出头,让其过期
if (L == queueMin.peekFirst()) queueMin.pollFirst();//弹出头,让其过期
// (2)(1)不能扩了,那换开头L再统计,L++;
// 新的L开头,**也不需要R再回到L重新刷一遍**,只需要去(1)继续判断就行。
//换头需要L++,换头也不需要R重新回来,继续判断就行
L++;
}
return ans;
}
public static void test(){
int[] arr = {3,2,1,4};
int target = 1;
System.out.println(getNum(arr, target));
System.out.println(validNumOfArr(arr, target));
}
public static void main(String[] args) {
test();
}
这个题,是不是很经典地运用了单调栈,求窗口内最大最小值
关键是,咱们在R++时,保持了单调性,统计结果时不需要R回退到L来重新求一遍——这个知识点是数组三连问题中,咱们就遇到过的知识点。要慢慢熟悉这种单调性带来的好处。这可以舍去那些不必要求的过程,加速算法!
但凡遇到这种舍弃的思想,题目挺难的,但是优化好了,能大大降低你的算法复杂度!!!
但凡遇到这种舍弃的思想,题目挺难的,但是优化好了,能大大降低你的算法复杂度!!!
但凡遇到这种舍弃的思想,题目挺难的,但是优化好了,能大大降低你的算法复杂度!!!
总结
提示:重要经验:
1)今后遇到求子数组窗口内的最大值,最小值,直接敏锐地敏感地想到要用单调栈!
2)如果求结果的过程中,发现双指针R++后,有一定的单调性,就不需要R回退了,这可以舍去那些不必要求的过程,加速算法!
3)但凡遇到这种舍弃的思想,题目挺难的,但是优化好了,能大大降低你的算法复杂度!!!
4)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。