5种解法的算法面试题 来看看你是青铜还是王者?

简介: 5种解法的算法面试题 来看看你是青铜还是王者?

先来详细描述下这道题。在一个全为正整数的数组中找到总和为给定值的子数组,给出子数组的起始下标(闭区间),举个例子:

在[3 2 1 2 3 4 5]这个数组中,和为10的子数组是[1 2 3 4],所以答案应该是[2,5]。

和为15的子数组是[1 2 3 4 5],答案为[2,6]。

这是一道非常有意思的题,为什么这么说?最简单的解法只要具备基本的编程知识就能写出,更优的解法需要你有数据结构和算法能力,越高效的解法越巧妙,可能你一下子无法想出所有的解法,但我相信你看完这篇博客一定会感叹算法的神奇。

回到这道题上来,实际上它有着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 &lt; sumArr.length; i++) {
            sumArr[i] = sumArr[i-1] + arr[i-1];  // 预处理,获取累计数组
        }
        for (int s = 0; s &lt; arr.length; s++) {
            for (int e = s+1; e &lt; 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 &lt; sumArr.length; i++) {
            sumArr[i] = sumArr[i-1] + arr[i-1];
        }
        for (int s = 0; s &lt; 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 &lt; r) {
            int mid = (l + r) &gt;&gt; 1;
            if (arr[mid] &gt;= 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&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();
        for (int i = 1; i &lt; sumArr.length; i++) {
            sumArr[i] = sumArr[i-1] + arr[i-1];
            map.put(sumArr[i], i-1);
        }
        for (int s = 0; s &lt; 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 &lt; arr.length) {
            if (sum &gt; target) {
                sum -= arr[s++];
            } else if (sum &lt; target) {
                sum += arr[++e];
            } else {
                return new int[]{s, e};
            }
        }
        return null;
    }

只有一层循环,时间复杂度O(n)。没有额外的空间占用,空间复杂度O(1),这就是最完美的解法。

总结

这道算法题乍看简单,细看其实真的不简单。可能你面试遇到,没办法一下子想到最优的解,但给出一个可行的解总比没有解强。我之前面试问别人这个题,他一上来就是想着怎么最优解决,反而连最简单的青铜解法都没写出来。记得下次面试,实在是解不出来就先给个60分的答案,然后再想办法把分数提升上去,别最后交了白卷。给出一个可行解,然后再持续迭代优化,我觉得这也是解决一个复杂问题比较好的思路。

最后送大家一句鸡汤,没有人生下来就是王者,只是不断的努力成为了王者罢了。

目录
相关文章
|
4月前
|
负载均衡 NoSQL 算法
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
这篇文章是关于Java面试中Redis相关问题的笔记,包括Redis事务实现、集群方案、主从复制原理、CAP和BASE理论以及负载均衡算法和类型。
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
|
2天前
|
存储 Java 索引
HashMap高频面试题,让你掌握青铜回答与王者级回答,你值得拥有
《HashMap高频面试题,让你掌握青铜回答与王者级回答,你值得拥有》介绍了HashMap的实现原理、数据存储、哈希冲突处理及扩容机制。文章通过对比JDK 1.7和JDK 1.8版本,详细解析了不同版本中的链表与红黑树结构、Entry与Node的区别,以及put()方法的具体实现。特别指出JDK 1.8引入红黑树优化查询性能,并采用尾插法避免多线程环境下的链表环问题。负载因子和扩容机制确保了HashMap在不同场景下的高效运行。
13 2
|
1月前
|
Java 编译器 程序员
Java面试高频题:用最优解法算出2乘以8!
本文探讨了面试中一个看似简单的数学问题——如何高效计算2×8。从直接使用乘法、位运算优化、编译器优化、加法实现到大整数场景下的处理,全面解析了不同方法的原理和适用场景,帮助读者深入理解计算效率优化的重要性。
35 6
|
2月前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩分享分库分表的基因算法设计,涵盖分片键选择、水平拆分策略及基因法优化查询效率等内容,助力面试者应对大厂技术面试,提高架构设计能力。
美团面试:百亿级分片,如何设计基因算法?
|
2月前
|
算法 前端开发 Java
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
这篇文章总结了单链表的常见面试题,并提供了详细的问题分析、思路分析以及Java代码实现,包括求单链表中有效节点的个数、查找单链表中的倒数第k个节点、单链表的反转以及从尾到头打印单链表等题目。
37 1
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
|
2月前
|
机器学习/深度学习 算法 Java
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
|
2月前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩在读者群中分享了关于分库分表的基因算法设计,旨在帮助大家应对一线互联网企业的面试题。文章详细介绍了分库分表的背景、分片键的设计目标和建议,以及基因法的具体应用和优缺点。通过系统化的梳理,帮助读者提升架构、设计和开发水平,顺利通过面试。
美团面试:百亿级分片,如何设计基因算法?
|
2月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
93 2
|
3月前
|
机器学习/深度学习 JavaScript 算法
面试中的网红虚拟DOM,你知多少呢?深入解读diff算法
该文章深入探讨了虚拟DOM的概念及其diff算法,解释了虚拟DOM如何最小化实际DOM的更新,以此提升web应用的性能,并详细分析了diff算法的实现机制。
|
4月前
|
消息中间件 存储 算法
这些年背过的面试题——实战算法篇
本文是技术人面试系列实战算法篇,面试中关于实战算法都需要了解哪些内容?一文带你详细了解,欢迎收藏!