实际上sum[s, e] = sum[0, e] - sum[0, s-1]
, sum[0,i]我们可以预先保存下来,然后重复使用。实际上sum数组我们可以通过一遍数据预处理获取到。上图中,arr蓝色区域的和正好等于sum数组中红色减去绿色,即sum(arr[3]-arr[7]) = sum[7]-sum[2]
。
回到代码上来,编码实现中我用了额外一个数组arrSum来存储0到i(0<=i<n)之间所有的和,为了处理方便sumArr下标从1开始,sumArr[i]表示远数组中sum[0, i-1]。有了sumArr之后,sum[s,e]就可以通过sumArr[e+1]-sumArr[s]间接获取到。完整代码如下:
public int[] find(int[] arr, int target) {
int[] sumArr = new int[arr.length + 1];
for (int i = 1; i < sumArr.length; i++) {
sumArr[i] = sumArr[i-1] + arr[i-1]; // 预处理,获取累计数组
}
for (int s = 0; s < arr.length; s++) {
for (int e = s+1; e < arr.length; e++) {
if (target == sumArr[e+1] - sumArr[s]) {
return new int[]{s, e};
}
}
}
return null;
}
通过上述用空间换时间的方式,我们可以直接将时间复杂度从O(n^3)
降低到O(n^2)
。
黄金-二分查找
细心的你可能已经发现了,因为给出的arr都是正整数,所以sumArr一定是递增且有序的,对于有序的数组,我们可以直接采用二分查找。对于这道题而已,我们可以遍历起点s,然在sumArr中二分去查找是否有终点e,如果s对于的e存在,那么sumArr[e]一定等于sumArr[s] + target,改造后的代码如下,相比于上面代码,增加了二分查找。
public int[] find(int[] arr, int target) {
int[] sumArr = new int[arr.length + 1];
for (int i = 1; i < sumArr.length; i++) {
sumArr[i] = sumArr[i-1] + arr[i-1];
}
for (int s = 0; s < arr.length; s++) {
int e = bSearch(sumArr, sumArr[s] + target);
if (e != -1) {
return new int[]{s, e};
}
}
return null;
}
// 二分查找
int bSearch(int[] arr, int target) {
int l = 1, r = arr.length-1;
while (l < r) {
int mid = (l + r) >> 1;
if (arr[mid] >= target) {
r = mid;
} else {
l = mid + 1;
}
}
if (arr[l] != target) {
return -1;
}
return l - 1;
}
由此,我们又继续将时间复杂从O(n^2)降低到了O(nlogn)。
钻石-HashMap优化
有序数组的查找除了可以用二分优化,还可以用hashMap来优化,借助HashMap O(1)的查询时间复杂度。我们又一次用空间来换取了时间。
public int[] find(int[] arr, int target) {
int[] sumArr = new int[arr.length + 1];
Map<Integer, Integer> map = new HashMap<>();
for (int i = 1; i < sumArr.length; i++) {
sumArr[i] = sumArr[i-1] + arr[i-1];
map.put(sumArr[i], i-1);
}
for (int s = 0; s < arr.length; s++) {
int e = map.getOrDefault(sumArr[s]+target, -1);
if (e != -1) {
return new int[]{s, e};
}
}
return null;
}
我们终于将时间复杂度降低到了O(n),这可是质的飞跃。
王者-尺取法
别急,还没结束,对于这道题还有王者解法。上文中我们通过不断的优化,将时间复杂度从O(n^3)一步步降低到了,但我们却一步步增加了存储的使用,从开始新增的sumArr数字,到最后的又增加的HashMap,空间复杂度从O(1)变为了O(n)。有没有办法把空间复杂度也给将下来?我能写到这那必然是有的。
这种算法叫做尺取法。尺取法,这个名字有点难理解。我们直接举个具体的例子,假设有n调长度不一的绳子并列放在一起,你需要找出其中连续的一部分绳子组成一条长度为target的绳子,这里需要注意是连续。这时候你可以找一个长度为target的尺子,然后把绳子一段段往尺子上放,如果发现短了就往后面再接一根,如果发现长了,就把最头上的一根扔掉,直到长度恰好合适。
在使用中我们并不需要这把尺子,只需要拿target作为标尺即可。说起来可能比较难理解,直接举个例子,下图演示了从数组中找到和为22的子数组的过程。
只要小了就右加,大了就左减,直到找到目标。
为什么尺取法是对的?我理解尺取法其实是解法二白银解法的一种优化,也是遍历了起点s,但是对终点e不做无效的遍历,如果e到某个位置后已经超了,因为数组里都是正数,再往后肯定是超的,也就没必要继续遍历e了。转而去调整s,如果s右移到某个位置后总和小了,s再往右总和只会更小,也就没必要继续调整s了…… 整个过程就像是先固定s去遍历e,然后固定e再去遍历s ……,直到得到结果。
尺取法可用的基础在于e往右移动总和一定是增的,s往右移总和一定是减的,也就是说数组中所有的数必须是正的。 没有完美的算法可以解决任何问题,但对于特定的问题一定有最完美的解法。
说完尺取法,我们来看下用尺取法是如何解决这道题的,代码比较简单,如下:
public int[] find(int[] arr, int target) {
int s = 0, e = 0;
int sum = arr[0];
while (e < arr.length) {
if (sum > target) {
sum -= arr[s++];
} else if (sum < target) {
sum += arr[++e];
} else {
return new int[]{s, e};
}
}
css
1,盒模型
2,如何实现一个最大的正方形
3,一行水平居中,多行居左
4,水平垂直居中
5,两栏布局,左边固定,右边自适应,左右不重叠
6,如何实现左右等高布局
7,画三角形
8,link @import导入css
9,BFC理解
js
1,判断 js 类型的方式
2,ES5 和 ES6 分别几种方式声明变量
3,闭包的概念?优缺点?
4,浅拷贝和深拷贝
5,数组去重的方法
6,DOM 事件有哪些阶段?谈谈对事件代理的理解
7,js 执行机制、事件循环
8,介绍下 promise.all
9,async 和 await,
10,ES6 的 class 和构造函数的区别
11,transform、translate、transition 分别是什么属性?CSS 中常用的实现动画方式,
12,介绍一下rAF(requestAnimationFrame)
13,javascript 的垃圾回收机制讲一下,
14,对前端性能优化有什么了解?一般都通过那几个方面去优化的?
件代理的理解
7,js 执行机制、事件循环
8,介绍下 promise.all
9,async 和 await,
10,ES6 的 class 和构造函数的区别
11,transform、translate、transition 分别是什么属性?CSS 中常用的实现动画方式,
12,介绍一下rAF(requestAnimationFrame)
13,javascript 的垃圾回收机制讲一下,
14,对前端性能优化有什么了解?一般都通过那几个方面去优化的?
[外链图片转存中…(img-3TNfi2Jj-1714514773441)]