一个算法面试题的5种不同解法

回到这道题上来,实际上它有着O(n^3) 、O(n^2) 、O(nlogn) 、O(n) 4种时间复杂度的解法,如果算上空间复杂度的差异的话总共5种解法,我觉得还是比较能考察到一个人的算法水平的。接下来让我带领大家由简入难看下从青铜到王者的5种解法,带大家吊打面试官。

这里我们设输入参数为(arr[],target),后续代码中我会用s和e来分别表示起始和终止位置。另外为了简化代码思路,我们假设给定的参数里最多只有一个解(实际上多个解也不难,但会让代码变长,不利于描述思路,多解的情况就留给你当课后作业了)。

青铜-暴力求解


首先当然是最简单的暴力求解了,遍历起始位置s和结束位置e,然后求s和e之间所有数字的和。三层循环简单粗暴,不需要任何的技巧,相信你大一刚学会编程就能解出来。

public int[] find(int[] arr, int target) {

for (int s = 0; s < arr.length; s++) {

for (int e = s+1; e < arr.length; e++) {

int sum = 0;

for (int k = s; k <= e; k++) { // 求s到e之间的和

sum += arr[k];

}

if (target == sum) {

return new int[]{s, e};

}

}

}

return null;

}

我们来分析下时间复杂度,很明显是O(n^3),当n超过1000时就会出现肉眼可见的慢,想想如何优化?

白银-空间换时间


上面代码中,我们每次都需要算从s到e之间的数组的和sum[s,e],假设我之前已经求过了[1,10]之间的和sum[1,10],现在要求[2,10]之间的和sum[2,10],显然这中间有很大一部分是重叠的(sum[2,10]),能不能把这部分重复扫描给消除掉?这里就需要做下巧妙的变换了。

实际上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的子数组的过程。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
81)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值