技术面试题是许多顶尖科技公司面试的主要内容,其中一些难题会令许多面试者望而却步,但其实这些题是有合理的解决方法的。git
多数求职者只是通读一遍问题和解法,囫囵吞枣。这比如试图单凭看问题和解法就想学会微积分。你得动手练习如何解题,单靠死记硬背效果不彰。程序员
就本书的面试题以及你可能遇到的其余题目,请参照如下几个步骤。面试
(1) 尽可能独立解题。本书后面有一些提示可供参考,但请尽可能不要依赖提示解决问题。许多题目确实难乎其难,可是不要紧,不要怕!此外,解题时还要考虑空间和时间效率。算法
(2) 在纸上写代码。在电脑上编程能够享受到语法高亮、代码完整、调试快速等种种好处,在纸上写代码则否则。经过在纸上多多实践来适应这种状况,并对在纸上编写、编辑代码之缓慢习觉得常。编程
(3) 在纸上测试代码。就是要在纸上写下通常用例、基本用例和错误用例等。面试中就得这么作,所以最好提早作好准备。数组
(4) 将代码照原样输入计算机。你也许会犯一大堆错误。请整理一份清单,罗列本身犯过的全部错误,这样在真正面试时才能牢记在心。缓存
此外,尽可能多作模拟面试。你和朋友能够轮流给对方作模拟面试。虽然你的朋友不见得受过什么专业训练,但至少能带你过一遍代码或者算法面试题。你也会在当面试官的体验中,受益良多。服务器
许多公司关注数据结构和算法面试题,并非要测试面试者的基础知识。然而,这些公司却默认面试者已具有相关的基础知识。数据结构
大多数面试官都不会问你二叉树平衡的具体算法或其余复杂算法。老实说,离开学校这么多年,恐怕他们本身也记不清这些算法了。app
通常来讲,你只要掌握基本知识便可。下面这份清单列出了必须掌握的知识。
数据结构 |
算法 |
概念 |
---|---|---|
链表 |
广度优先搜索 |
位操做 |
树、单词查找树、图 |
深度优先搜索 |
内存(堆和栈) |
栈和队列 |
二分查找 |
递归 |
堆 |
归并排序 |
动态规划 |
向量/数组列表 |
快排 |
大 |
散列表 |
|
|
对于上述各项题目,务必掌握它们的具体用法、实现方法、应用场景以及空间和时间复杂度。
一种不错的方法就是练习如何实现数据结构和算法(先在纸上,而后在电脑上)。你会在这个过程当中学到数据结构内部是如何工做的,这对不少面试而言都是不可或缺的。
你错过上面那段了吗?千万不要错过,这很是重要。若是对上面列出的某个数据结构和算法感受不能运用自如,就从头开始练习吧。
其中,散列表是必不可少的一个题目。对这个数据结构,务必要成竹在胸。
下面这张表会在不少涉及可扩展性或者内存排序限制等问题上助你一臂之力。尽管不强求你记下来,但是记住总会有用。你至少应该轻车熟路。
2的幂 |
准确值(X) |
近似值 |
X字节转换成MB、GB等 |
---|---|---|---|
7 |
128 |
|
|
8 |
256 |
|
|
10 |
1024 |
一千 |
1 K |
16 |
65 536 |
|
64 K |
20 |
1 048 576 |
一百万 |
1 MB |
30 |
1 073 741 824 |
十亿 |
1 GB |
32 |
4 294 967 296 |
|
4 GB |
40 |
1 099 511 627 776 |
一万亿 |
1 TB |
这张表能够拿来作速算。例如,一个将每一个32位整数映射成布尔值的向量表能够在一台普通计算机内存中放下。那样的整数有个。由于每一个整数只占位向量表中的一位,共须要
位(或者
字节)来存储该映射表,大约是千兆字节的一半,普通机器很容易知足。
在接受互联网公司的电话面试时,不妨把表放在眼前,也许能派上用场。
下面的流程图将教你如何逐步解决一个问题。要学以至用。你能够从CrackingTheCodingInterview.com下载这个提纲及更多内容。
接下来我会详述该流程图。
面试本就困难。若是你没法马上得出答案,那也没有关系,这很正常,并不表明什么。
注意听面试官的提示。面试官有时热情洋溢,有时却意兴阑珊。面试官参与程度取决于你的表现、问题的难度以及该面试官的期待和个性。
当你被问到一个问题或者当你在练习时,按下面的步骤完成解题。
也许你之前听过这个常规性建议:确保听清楚题。但我给你的建议不止这一点。
固然了,你首先要保证听清题,其次弄清楚模棱两可的地方。
可是我要说的不止如此。
举个例子,假设一个问题如下列其中一个话题做为开头,那么能够合理地认为它给出的全部信息都并不是无缘无故的。
“有两个排序的数组,找到……”
你极可能须要注意到数据是有序的。数据是否有序会致使最优算法截然不同。
“设计一个在服务器上常常运行的算法……”
在服务器上/重复运行不一样于只运行一次的算法。也许这意味你能够缓存数据,或者意味着你能够瓜熟蒂落地对数据集进行预处理。
若是信息对算法没影响,那么面试官不大可能(尽管也不无可能)把它给你。
不少求职者都能准确听清问题。可是开发算法的时间只有短短的十来分钟,以致于解决问题的一些关键细节被忽略了。这样一来不管怎样都没法优化问题了。
你的初版算法确实不须要这些信息。可是若是你陷入瓶颈或者想寻找更优方案,就回头看看有没有错过什么。
即便把相关信息写在白板上也会对你大有裨益。
画个例图能显著提升你的解题能力,尽管如此,还有如此多的求职者只是试图在脑海中解决问题。
当你听到一道题时,离开椅子去白板上画个例图。
不过画例图是有技巧的。首先你须要一个好例子。
一般状况下,以一棵二叉搜索树为例,求职者可能会画以下例图。
这是个很糟糕的例子。第一,过小,不容易寻找模式。第二,不够具体,二叉搜索树有值。若是那些数字能够帮助你处理这个问题怎么办?第三,这其实是个特殊状况。它不只是个平衡树,也是个漂亮、完美的树,其每一个非叶节点都有两个子节点。特殊状况极具欺骗性,对解题无益。
实际上,你须要设计一个这样的例子。
尽力作出最好的例子。若是后面发现你的例子不那么正确,你应该修复它。
一旦完成了例子(其实,你也能够在某些问题中调换7.3.1.2步和7.3.1.3步的顺序),就给出一个蛮力法。你的初始算法不怎么好也没有关系,这很正常。
一些求职者不想给出蛮力法,是由于他们认为此方法不只显而易见并且糟糕透顶。可是事实是:即便对你来讲垂手可得,也未必对全部求职者来讲都这样。你不会想让面试官认为,即便解出这一简单算法对你来讲也得绞尽脑汁。
初始解法很糟糕,这很正常,没必要介怀。先说明该解法的空间和时间复杂度,再开始优化。
你一旦有了蛮力法,就应该努力优化该方法。如下技巧就有了用武之地。
(1) 寻找未使用的信息。你的面试官告诉过你数组是有序的吗?你如何利用这些信息?
(2) 换个新例子。不少时候,换个不一样的例子会让你思路畅通,看到问题模式所在。
(3) 尝试错误解法。低效的例子能帮你看清优化的方法,一个错误的解法可能会帮助你找到正确的方法。比方说,若是让你从一个全部值可能都相等的集合中生成一个随机值。一个错误的方法多是直接返回半随机值。能够返回任何值,可是可能某些值几率更大,进而思考为何解决方案不是完美随机值。你能调整几率吗?
(4) 权衡时间、空间。有时存储额外的问题相关数据可能对优化运行时间有益。
(5) 预处理信息。有办法从新组织数据(排序等)或者预先计算一些有助于节省时间的值吗?
(6) 使用散列表。散列表在面试题中用途普遍,你应该第一个想到它。
(7) 考虑可想象的极限运行时间(详见7.9节)。
在蛮力法基础上试试这些技巧,寻找BUD的优化点。
明确了最佳算法后,不要急于写代码。花点时间巩固对该算法的理解。
白板编程很慢,慢得超乎想象。测试、修复亦如此。所以,要尽量地在一开始就确保思路近乎完美。
梳理你的算法,以了解它须要什么样的结构,有什么变量,什么时候发生改变。
伪代码是什么?若是你更愿意写伪代码,没有问题。可是写的时候要小心。基本的步骤((1) 访问数组。(2) 找最大值。(3) 堆插入。)或者简明的逻辑(if
p < q
, movep
. else moveq
.)值得一试。可是若是你用简单的词语表明for
循环,基本上这段代码就烂透了,除了代码写得快以外一无可取。
你若是没有完全理解要写什么,就会在编程时举步维艰,这会致使你用更长的时间才能完成,而且更容易犯大错。
这下你已经有了一个最优算法而且对全部细节都了如指掌,接下来就是实现算法了。
写代码时要从白板的左上角(要省着点空间)开始。代码尽可能沿水平方向写(不要写成一条斜线),不然会乱做一团,而且像Python那样对空格敏感的语言来讲,读起来会云里雾里,使人困惑。
切记:你只能写一小段代码来证实本身是个优秀的开发人员。所以,每行代码都相当重要,必定要写得漂亮。
写出漂亮代码意味着你要作到如下几点。
{{1, 2, 3}, {4, 5, 6}, ...}
,不要浪费时间去写初始化的代码。能够伪装本身有个函数initIncrementalMatrix(int size)
,稍后须要时再回头写完它。todo
,这样只需解释清楚你想测试什么就能够了。StartEndPair
(或者Range
)对象看成list
返回。你不须要去把这个类写完,大可假设有这样一个类,后面若是有富裕时间再补充细节便可。for
循环)使用i
和j
就不对。可是,使用i
和j
时要多加当心。若是写了相似于int i = startOfChild(array)
的变量名称,可能还能够使用更好的名称,好比startChild
。然而,长的变量名写起来也会比较慢。你能够除第一次之外都用缩写,多数面试官都能赞成。比方说你第一次能够使用startChild
,而后告诉面试官后面你会将其缩写为sc
。
评价代码好坏的标准因面试官、求职者、题目的不一样而有所变化。因此只要专心写出一手漂亮的代码便可,尽人事、知天命。
若是发现某些地方须要稍后重构,就和面试官商量一下,看是否值得花时间重构。一般都会获得确定答复,偶尔不是。
若是以为一头雾水(这很常见),就再回头过一遍。
在现实中,不通过测试就不会签入代码;在面试中,未通过测试一样不要“提交”。
测试代码有两种办法:一种聪明的,一种不那么聪明的。
许多求职者会用最开始的例子来测试代码。那样作可能会发现一些bug,但一样会花很长时间。手动测试很慢。若是设计算法时真的使用了一个大而好的例子,那么测试时间就会很长,但最后可能只在代码末尾发现一些小问题。
你应该尝试如下方法。
(1) 从概念测试着手。概念测试就是阅读和分析代码的每一行。像代码评审那样思考,在心中解释每一行代码的含义。
(2) 跳着看代码。重点检查相似x = length-2
的行。对于for
循环,要尤其注意初始化的地方,好比i = 1
。当你真的去检查时,就很容易发现小错误。
(3) 热点代码。若是你编程经验足够丰富的话,就会知道哪些地方可能出错。递归中的基线条件、整数除法、二叉树中的空节点、链表迭代中的开始和结束,这些要反复检查才行。
(4) 短小精悍的用例。接下来开始尝试测试代码,使用真实、具体的用例。不要使用大而全的例子,好比前面用来开发算法的8元素数组,只须要使用3到4个元素的数组就够了。这样也能够发现相同的bug,但比大的快多了。
(5) 特殊用例。用空值、单个元素、极端状况和其余特殊状况检测代码。
发现了bug(极可能会)就要修复。但注意不要贸然修改。仔细斟酌,找出问题所在,找到最佳的修改方案,只有这样才能动手。
这也许是我找到的优化问题最有效的方法了。BUD是如下词语的首字母缩写:
以上是最多见的3个问题,而面试者在优化算法时每每会浪费时间于此。你能够在蛮力法中找找它们的影子。发现一个后,就能够集中精力来解决。
若是这样仍没有获得最佳算法,也能够在当前最好的算法中找找这3类优化点。
瓶颈就是算法中拖慢总体运行时间的某部分。一般会以两种方式出现。
一次性的工做会拖累整个算法。例如,假设你的算法分为两步,第一步是排序整个数组,第二步是根据属性找到特定元素。第一步是,第二步是
。尽管能够把第二步时间优化到
甚至
,但那又有什么用呢?聊胜于无而已。它不是当务之急,由于
才是瓶颈。除非优化第一步,不然你的算法总体上一直是
。
你有一块工做不断重复,好比搜索。也许你能够把它从降到
甚至
。这样就大大加快了总体运行时间。
优化瓶颈,对总体运行时间的影响是立竿见影的。
举个例子:有一个值都不相同的整数数组,计算两个数差值为
的对数。例如,数组
{1, 7, 5, 9, 2, 12, 3}
,差值为2,差值为2的一共有4对:(1, 3)、(3, 5)、(5, 7)、(7, 9)。
用蛮力法就是遍历数组,从第一个元素开始搜索剩下的元素(即一对中的另外一个)。对于每一对,计算差值。若是差值等于,计数加一。
该算法的瓶颈在于重复搜索对数中的另外一个。所以,这是接下来优化的重点。
怎么才能更快地找到正确的另外一个?已知的另外一个,即
或
。若是把数组排序,就能够用二分查找来找到另外一个,
个元素的话查找的时间就是
。
如今,将算法分为两步,每一步都用时。接下来,排序构成新的瓶颈。优化第二步于事无补,由于第一步已经拖慢了总体运行时间。
必须彻底丢弃第一步排序数组,只使用未排序的数组。那如何在未排序的数组中快速查找呢?借助散列表吧。
把数组中全部元素都放到散列表中。而后判断或者
是否存在。只是过一遍散列表,用时为
。
举个例子:打印知足
的全部正整数解,其中
、
、
、
是1至1000间的整数。
用蛮力法来解会有四重for
循环,以下:
1. n = 1000 2. for a from 1 to n 3. for b from 1 to n 4. for c from 1 to n 5. for d from 1 to n 6. if a3 + b3 == c3 + d3 7. print a, b, c, d
用上面算法迭代、
、
、
全部可能,而后检测是否知足上述表达式。
在找到一个可行解后,就不用继续检查的其余值了。由于
的一次循环中只有一个值能知足。因此一旦找到可行解至少应该跳出循环。
1. n = 1000 2. for a from 1 to n 3. for b from 1 to n 4. for c from 1 to n 5. for d from 1 to n 6. if a3 + b3 = c3 + d3 7. print a, b, c, d 8. break // 跳出d循环
虽然该优化对运行时间并没有改变,运行时间还是,但仍值得一试。
还有其余无用功吗?答案是确定的,对于每一个 ,均可以经过
这个简单公式获得
。
1. n = 1000 2. for a from 1 to n 3. for b from 1 to n 4. for c from 1 to n 5. d = pow(a3 + b3 - c3, 1/3) // 取整成int 6. if a3 + b3 == c3 + d3 && 0 <= d && d <= n // 验证结果 7. print a, b, c, d
第6行的if
语句相当重要,由于第5行每次都会找到一个的值,可是须要检查是不是正确的整数值。
这样一来,运行时间就从降到了
。
沿用上述问题及蛮力法,此次来找一找有哪些重复性工做。
这个算法本质上遍历全部对的可能性,而后寻找全部
对的可能性,找到和
对匹配的对。
为何对于每一对都要计算全部
对的可能性?只需一次性建立一个
对列表,而后对于每一个
对,都去
列表中寻找匹配。想要快速定位
对,对
列表中每一个元素,均可以把
对的和看成键,
看成值(或者知足那个和的对列表)插入到散列表。
1. n = 1000 2. for c from 1 to n 3. for d from 1 to n 4. result = c3 + d3 5. append (c, d) to list at value map[result] 6. for a from 1 to n 7. for b from 1 to n 8. result = a3 + b3 9. list = map.get(result) 10. for each pair in list 11. print a, b, pair
实际上,已经有了全部对的散列表,大可直接使用。不须要再去生成
对。每一个
都已在散列表中。
1. n = 1000
2. for c from 1 to n
3. for d from 1 to n
4. result = c<sup>3</sup> + d<sup>3</sup>
5. append (c, d) to list at value map[result]
6.
7. for each result, list in map
8. for each pair1 in list
9. for each pair2 in list
10. print pair1, pair2复制代码
它的运行时间是。
第一次遇到如何在排序的数组中寻找某个元素(习得二分查找以前),你可能不会一会儿想到:“啊哈!咱们能够比较中间值和目标值,而后在剩下的一半中递归这个过程。”
然而,若是让一些没有计算机科学背景的人在一堆按字母表排序的论文中寻找指定论文,他们可能会用到相似于二分查找的方式。他们估计会说:“天哪,Peter Smith?可能在这堆论文的下面。”而后随机选择一个中间的(例如i,s,h开头的)论文,与Peter Smith作比较,接着在剩余的论文中继续用这个方法查找。尽管他们不知道二分查找,但能够凭直觉“作出来”。
咱们的大脑颇有趣。干巴巴地抛出像“设计一个算法”这样的题目,人们常常会搞得乱七八糟。可是若是给出一个实例,不管是数据(例如数组)仍是现实生活中其余的相似物(例如一堆论文),他们就会凭直觉开发出一个很好的算法。
我已经无数次地看到这样的事发生在求职者身上。他们在计算机上完成的算法奇慢无比,但一旦被要求人工解决一样问题,立马干净利落地完成。
所以,当你遇到一个问题时,一个好办法是尝试在直观的真实例子上凭直觉解决它。一般越大的例子越容易。
举个例子:给定较小字符串
s
和较大字符串b
,设计一个算法,寻找在较大字符串中较小字符串的全部排列,打印每一个排列的位置。
考虑一下你要怎么解决这道题。注意排列是字符串的重组,所以s
中的字符能以任何顺序出如今b
中,可是它们必须是连续的(不被其余字符隔开)。
像大多数求职者同样,你可能会这么想:先生成s
的全排列,而后看它们是否在b
中。全排列有种,所以运行时间是
,其中
是
s
的长度,是
b
的长度。
这样是可行的,但实在慢得太离谱了。实际上该算法比指数级的算法还要糟糕透顶。若是s
有14个字符,那么会有超过870亿个全排列。s
每增长一个字符,全排列就会增长15倍。天哪!
换种不一样的方式,就能够垂手可得地开发出一个还不错的算法。参考以下例子:
s:abbc
b:cbabadcbbabbcbabaabccbabc复制代码
b
中s
的全排列在哪儿?不要管如何作,找到它们就行。很简单的,12岁的小孩子都能作到!
(真的,赶忙去找,我等你。)
我已经在每一个全排列下面画了线。
s: abbc
b: cbabadcbbabbcbabaabccbabc
———— ———— ————
———— ———— ————
————复制代码
你找到了吗?怎么作的?
不多有人——即便以前提出算法的人——真的去生成
abbc
的全排列,再去b
中逐个寻找。几乎全部人都采用了以下两种方式(很是类似)之一。
(1) 遍历b
,查看4个字符(由于s
中只有4个字符)的滑动窗口。逐一检查窗口是不是s
的一个全排列。
(2) 遍历b
。每次发现一个字符在s
中时,就去检查它日后的4个(包括它)字符是否属于s
的全排列。
取决于“是不是一个全排列”的具体实现方式,你获得的运行时多是、
或者
。尽管这些都不是最优算法(包含
算法),但已经比咱们以前的好太多。
解题时,试试这个方法。使用一个大而好的例子,直观地手动解决这个特定例子。而后复盘,思考你是如何解决它的。反向设计算法。
重点留意你凭直觉或不经意间作的任何“优化”。例如,解题时你可能会跳过以d
开头的窗口,由于d
不在abbc
中。这是你靠大脑作出的一个优化,在设计算法时也应该留意到。
咱们经过简化来实现一个由多步骤构成的方法。首先,能够简化或者调整约束,好比数据类型。这样一来,就能够解决简化后的问题了。最后,调整这个算法,让它适应更为复杂的状况。
举个例子:能够经过从杂志上剪下词语拼凑成句来完成一封邀请函。如何分辨一封邀请函(以字符串表示)是否能够从给定杂志(字符串)中获取呢?
为了简化问题,能够把从杂志上剪下词语改成剪下字符。
经过建立一个数组并计数字符串,能够解决邀请函的字符串简化版问题,其中数组中的每一位对应一个字母。首先计算每一个字符在邀请函中出现的次数,而后遍历杂志查看是否能知足。
推导出这个算法,意味着咱们作了相似的工做。不一样的是,此次不是建立一个字符数组来计数,而是建立一个单词映射频率的散列表。
咱们能够由浅入深,首先解决一个基本状况(例如,),而后尝试从这里开始构建。遇到更复杂或者有趣的状况(一般是
或者
)时,尝试使用以前的方法解决。
举个例子:设计一个算法打印出字符串的全部排列组合。简单起见,假设全部字符均不相同。
思考一个测试字符串abcdefg
。
用例 "a" --> {"a"}
用例 "ab" --> {"ab", "ba"}
用例 "abc" --> ?复制代码
这是第一个“有趣”的状况。若是已经有了P("ab")
的答案,如何获得P("abc")
的答案呢?已知可选的字母是c
,所以能够在每种可能中插入c
,即以下模式。
P("abc") = 把"c"插入到 P("ab")中的全部字符串的全部位置
P("abc") = 把"c"插入到{"ab","ba"}中的全部字符串的全部位置
P("abc") = 合并({"cab", "acb", "abc"}, {"cba", "bca", bac"})
P("abc") = {"cab", "acb", "abc", "cba", "bca", bac"}复制代码
理解了这个模式后,就能够写个差很少的递归算法了。经过“截断末尾字符”的方式,能够生成s1...sn
字符串的全部组合。作法很简单,首先生成字符串s1...sn-1
的全部组合,而后遍历全部组合,每一个字符串的每一个位置都插入sn
获得新的字符串。
这种由基础例子逐渐推导的方法一般会获得一个递归算法。
这种方法很取巧但奏效。咱们能够简单过一遍全部的数据结构,一个个地试。这种方法之因此有效在于,一旦数据结构(比方说树)选对了,解题可能就简单了,手到擒来。
举个例子:随机产生数字并放入(动态)数组。你怎么记录它每一步的中间值?
应用数据结构头脑风暴法的过程可能以下所示。
总的来讲,你解决过的问题越多,就越擅于选择出合适的数据结构。不只如此,你的直觉还会变得更加敏锐,能判断出哪一种方法最为行之有效。
考虑到可想象的极限运行时间(BCR),可能对解决某些问题大有裨益。
可想象的极限运行时间,按字面意思理解就是,关于某个问题的解决,你能够想象出的运行时间的极限。你能够垂手可得地证实,BCR是没法超越的。
比方说,假设你想计算两个数组(长度分别为、
)共有元素的个数,会立马想到用时不可能超过
,由于必需要访问每一个数组中的全部元素,因此
就是可想象的极限运行时间。
或者,假设你想打印数组中全部成对值。你固然明白用时不可能超过,由于有
对须要打印。
不过还要注意。假设面试官要求你在一个数组中(假定全部元素均不一样)找到全部和为的对。一些对可想象的极限运行时间概念只知其一;不知其二的求职者可能会说BCR是
,理由是不得不访问
对。
这种说法大错特错。仅仅由于你想要全部和为特定值的对,并不意味着必须访问全部对。事实上根本不须要。
可想象的极限运行时间与最佳运行时间(best case runtime)有什么关系呢?绝不相干!可想象的极限运行时间是针对一个问题而言,在很大程度上是一个输入输出的函数,和特定的算法并没有关系。事实上,若是计算可想象的极限运行时间时还要考虑具体用到哪一个算法,那就极可能作错了。最佳运行时间是针对具体算法(一般是一个毫无心义的值)的。
注意,可想象的极限运行时间不必定能够实现。它的意义在于告诉你用时不会超过该时间。
问题:找到两个排序数组中相同元素的个数,这两个数组长度相同,且每一个数组中元素都不一样。
从以下这个经典例子着手,在共同元素下标注下划线。
A: 13 27 35 40 49 55 59 B: 17 35 39 40 55 58 60
解出这道题使用的是蛮力法,即对于A
中的每一个元素都去B
中搜索。这须要花费的时间,由于对于
A
中的每一个元素(共个)都须要在
B
中作的搜索。
BCR为,由于咱们知道每一个元素至少访问一次,一共
个元素。若是跳过一个元素,那么这个元素是否有相同的值会影响最后的结果。例如,若是从没有访问过
B
中的最后一个元素,那么把60改为59,结果就不对了。
回到正题。如今有一个的算法,咱们想要更好地优化该算法,但不必定要像
那样快。
Brute Force: O(N2) Optimal Algorithm: ? BCR: O(N)
与
之间的最优算法是什么?有许多,准确地讲,有无穷无尽。理论上能够有个算法是
。然而,不管是在面试仍是现实中,运行时间都不太多是这样。
请记住这个问题,由于它在面试中淘汰了不少人。运行时间不是一个多选题。虽然常见的运行时间有
、
、
、
或者
,但你不应直接假设某个问题的运行时间是多少而不考虑推导的过程。事实上,当你对运行时间是多少百思不解时,不妨猜一猜。这时你最有可能遇到一个不太明显、不太常见的运行时间。也许是
,
是数组的大小,
是数值对的个数。合理推导,不要只靠猜。
最有可能的是,咱们正努力推导出或者
算法。这说明什么呢?
若是当前算法的运行时间是,那么想获得
或者
可能意味着要把第二个
优化成
或者
。
这是BCR的一大益处,咱们能够经过运行时间获得关于优化方向的启示。
第二个来自于搜索。已知数组是排序的,能够用快于
的时间在排序的数组中搜索吗?
固然能够了,用二分查找在一个排序的数组中寻找一个元素的运行时间是。
如今咱们把算法优化为。
Brute Force: O(N2) Improved Algorithm: O(N log N) Optimal Algorithm: ? BCR: O(N)
还能继续优化吗?继续优化意味着把 缩短为
。
一般状况下,二分查找在排序数组中的最快运行时间是。但此次不是正常状况,咱们一直在重复搜索。
BCR告诉咱们,解出这个算法的最快运行时间为。所以,咱们所作的任何
的工做都是“免费的”,不会影响到运行时间。
重读7.3.1节关于优化的技巧,是否有一些能够派上用场呢?
一个技巧是预计算或者预处理。任何时间内的预处理都是“免费的”。这不会影响运行时间。
这又是BCR的一大益处。任何你所作的不超过或者等于BCR的工做都是“免费的”,从这个意义上来讲,对运行时间并没有影响。你可能最终会将此剔除,可是目前不是当务之急。
重中之重仍在于将搜索由减小为
。任何
或者不超过
时间内的预计算都是“免费的”。
所以,能够把B
中全部数据都放入散列表,它的运行时间是,而后只须要遍历
A
,查看每一个元素是否在散列表中。查找(搜索)时间是,因此总的运行时间是
。
假设面试官问了一个让咱们坐立不安的问题:还能继续优化吗?
答案是不能够,这里指运行时间。咱们已经实现了最快的运行时间,所以没办法继续优化大时间,倒能够尝试优化空间复杂度。
这是BCR的另外一大益处。它告诉咱们运行时间优化的极限,咱们到这儿就该调转枪头,开始优化空间复杂度了。
事实上,就算面试官不主动要求,咱们也应该对算法抱有疑问。就算不存储数据,也能够精确地得到相同的运行时间。那么为何面试官给出了排序的数组?并不是不寻常,只是有些奇怪罢了。
回到咱们的例子:
A: 13 27 35 40 49 55 59 B: 17 35 39 40 55 58 60
要找有以下特征的算法。
不使用其余空间的最佳算法是二分查找。想想怎么优化它。试着过一遍整个算法。
(1) 用二分查找在B
中找 A[0] = 13
。没找到。
(2) 用二分查找在B
中找 A[1] = 27
。没找到。
(3) 用二分查找在B
中找 A[2] = 35
。在B[1]
中找到。
(4) 用二分查找在B
中找 A[3] = 40
。在B[5]
中找到。
(5) 用二分查找在B
中找 A[4] = 49
。没找到。
(6) ……
想一想BUD。搜索是瓶颈。整个过程有多余或者重复性工做吗?
搜索A[3] = 40
不须要搜索整个B
。在B[1]
中已找到35,因此40不可能在35前面。
每次二分查找都应该从上次终止点的左边开始。
实际上,根本不须要二分查找,大可直接借助线性搜索。只要在B
中的线性搜索每次都从上次终止的左边出发,就知道将要用线性时间进行搜索。
(1) 在B
中线性搜索 A[0] = 13
,开始于 B[0] = 17
,结束于 B[0] = 17
。未找到。
(2) 在B
中线性搜索 A[1] = 27
,开始于 B[0] = 17
,结束于 B[1] = 35
。未找到。
(3) 在B
中线性搜索 A[2] = 35
,开始于 B[1] = 35
,结束于 B[1] = 35
。找到。
(4) 在B
中线性搜索 A[3] = 40
,开始于 B[2] = 39
,结束于 B[3] = 40
。找到。
(5) 在B
中线性搜索 A[4] = 49
,开始于 B[3] = 40
,结束于 B[4] = 55
。找到。
(6) ……
以上算法与合并排序数组一模一样。该算法的运行时间为,空间为
。
如今同时达到了BCR和最小的空间占用,这已是极限了。
这是另外一个使用BCR的方式。若是达到了BCR而且其余空间为
,那么不管是大
时间仍是空间都已经没法优化。
BCR不是一个真正的算法概念,也没法在算法教材中找到其身影。但我我的以为其大有用处,无论是在我本身解题时,仍是在指导别人解题时。
若是很难掌握它,先确保你已经理解了大时间的概念。你要作到运用自如。一旦你掌握了,弄懂BCR不过是小菜一碟。
流传最广、危害最大的谣言就是,求职者必须答对每一个问题。这种说法并不全对。
首先,面试的回答不该该简单分为“对”或“不对”。当我评价一我的在面试中的表现时,从不会想:“他答对了多少题?”评价不是非黑即白。相反地,评价应该基于最终解法有多理想,解题花了多长时间,须要多少提示,代码有多干净。这些才是关键。
其次,评价面试表现时,要和其余的候选人作对比。例如,若是你优化一个问题须要15分钟,别人解决一个更容易的问题只须要5分钟,那么他就比你表现好吗?也许是,也许不是。若是给你一个显而易见的问题,面试官可能会但愿你干净利落地给出最优解法。可是若是是难题,那么犯些错也是在乎料之中的。
最后,许多或者绝大多数的问题都不简单,就算一个出类拔萃的求职者也很难马上给出最优算法。一般来讲,对于我提出的一些问题,厉害的求职者也要20到30分钟才能解出。
我在谷歌评估过成千上万份求职者的信息,也只看到过一个求职者天衣无缝地经过了面试。其余人,包括收到录用通知的人,都或多或少犯过错。
若是你曾见过某个面试题,要提早说明。面试官问你这些问题是为了评估你解决问题的能力。若是你已经知道某个题的答案了,他们就没法准确无误地评估你的水平了。
此外,若是你对本身见过这道题讳莫如深,面试官还可能会发现你为人不诚实。反过来讲,若是你坦白了这一点,就会给面试官留下诚实的好印象。
在不少顶级公司,面试官并不在意你用什么语言。相比之下,他们更在意你解决问题的能力。
不过,也有些公司比较关注某种语言,乐于看到你是如何驾轻就熟地使用该语言编写代码的。
若是你能够任意选择语言的话,就选最为驾轻就熟的。
话虽如此,若是你擅长几种语言,就将如下几点牢记于心。
这一点不强求。可是若面试官知道你所使用的语言,多是最为理想的。从这点上讲,更流行的语言可能更为合适。
即便面试官不知道你所用的语言,他们也但愿能对该语言有个大体了解。一些语言的可读性天生就优于其余语言,由于它们与其余语言有类似之处。
举个例子,Java很容易理解,即便没有用过它的人也能看懂。绝大多数人都用过与Java语法相似的语言,好比C和C++。
然而,像Scala和Objective C这样的语言,其语法就大不相同了。
使用某些语言会带来潜在的问题。例如,使用C++就意味着除了代码中常见的bug,还存在内存管理和指针的问题。
有些语言更为冗长烦琐。Java就是一个例子,与Python相比,该语言极为烦琐。经过比较如下代码就一目了然了。
Python:
1. dict = {"left": 1, "right": 2, "top": 3, "bottom": 4};复制代码
Java:
1. HashMap<String, Integer> dict = new HashMap<String, Integer>().
2. dict.put("left", 1);
3. dict.put("right", 2);
4. dict.put("top", 3);
5. dict.put("bottom", 4);复制代码
能够经过缩写使Java更为简洁。好比一个求职者能够在白板上这样写:
1. HM<S, I> dict = new HM<S, I>().
2. dict.put("left", 1);
3. ... "right", 2
4. ... "top", 3
5. ... "bottom", 4复制代码
你须要解释这些缩写,但绝大多数面试官并不在乎。
有些语言使用起来更为容易。例如,使用Python能够垂手可得地让一个函数返回多个值。可是若是使用Java,就还须要一个新的类。语言的易用性可能对解决某些问题大有裨益。
与上述相似,能够经过缩写或者实际上不存在的假设方法让语言更易使用。例如,若是一种语言提供了矩阵转置的方法而另外一种语言未提供,也并不必定要选第一种语言(若是面试题须要那个函数的话),能够假设另外一种语言也有相似的方法。
到目前为止,你可能知道雇主想看到你写出一手“漂亮的、干净的”代码。但具体的标准是什么呢?在面试中又如何体现呢?
通常来说,好代码应符合如下标准。
追求这些须要掌握好平衡。好比,有时牺牲必定的效率来提升可维护性就是明智之举,反之亦然。
在面试中写代码时应该考虑到这些。如下内容更为具体地阐述了好代码的标准。
假设让你写一个函数,把两个单独的数学表达式相加,形如(其中系数和指数能够为任意正实数或负实数),即该表达式是由一系列项组成,每一个项都是一个常数乘以一个指数。面试官还补充说,不但愿你解析字符串,但你能够使用任何数据结构。
这有几种不一样的实现方式。
一个糟糕透顶的实现方式是把表达式放在一个double
的数组中,第个元素对应表达式中
项的系数。这个数据结构的问题在于,不支持指数为负数或非整数的表达式,还要求1000个元素大小的数组来存储表达式
。
1. int[] sum(double[] expr1, double[] expr2) {
2. ...
3. }复制代码
稍差的方案是用两个数组分别保存系数和指数。用这种方法,表达式的每一项都有序保存,但能“匹配”。第项就表示为
oefficients[i]*xexponents[i]
。
对于这种实现方式,若是coefficients[p] = k
而且exponents[p] = m
,那么第项就是
。虽然这样没有了上一种方式的限制,但仍然显得杂乱无章。一个表达式却须要使用两个数组。若是两个数组长度不一样,表达式可能有“未定义”的值。不只如此,返回也让人不胜其烦,由于要返回两个数组。
1. ??? sum(double[] coeffs1, double[] expon1, double[] coeffs2, double[] expon2) {
2. ...
3. }复制代码
一个好的实现方式就是为这个问题中的表达式设计数据结构。
1. class ExprTerm {
2. double coefficient;
3. double exponent;
4. }
5.
6. ExprTerm[] sum(ExprTerm[] expr1, ExprTerm[] expr2) {
7. ...
8. }复制代码
有些人可能认为甚至声称,这是“过分优化”。无论是否是,也无论你有没有以为这是过分优化,关键在于上面的代码体现了你在思考如何设计代码,而不是以最快速度将一些数据东拼西凑。
假设让你写一个函数来检查是否一个二进制的值(以字符串表示)等于用字符串表示的一个十六进制数。
解决该问题的一种简单方法就是复用代码。
1. boolean compareBinToHex(String binary, String hex) {
2. int n1 = convertFromBase(binary, 2);
3. int n2 = convertFromBase(hex, 16);
4. if (n1 < 0 || n2 < 0) {
5. return false;
6. }
7. return n1 == n2;
8. }
9.
10. int convertFromBase(String number, int base) {
11. if (base < 2 || (base > 10 && base != 16)) return -1;
12. int value = 0;
13. for (int i = number.length() - 1; i >= 0; i--) {
14. int digit = digitToValue(number.charAt(i));
15. if (digit < 0 || digit >= base) {
16. return -1;
17. }
18. int exp = number.length() - 1 - i;
19. value += digit * Math.pow(base, exp);
20. }
21. return value;
22. }
23.
24. int digitToValue(char c) { ... }复制代码
能够单独实现二进制转换和十六进制转换的代码,但这只会让代码难写且难以维护。不如写一个convertFromBase
方法和 digitToValue
方法,而后复用代码。
编写模块化的代码时要把独立代码块放到各自的方法中。这有助于提升代码的可维护性、可读性和可测试性。
想象你正在写一个交换数组中最小数和最大数的代码,能够用以下方法完成。
1. void swapMinMax(int[] array) {
2. int minIndex = 0;
3. for (int i = 1; i < array.length; i++) {
4. if (array[i] < array[minIndex]) {
5. minIndex = i;
6. }
7. }
8.
9. int maxIndex = 0;
10. for (int i = 1; i < array.length; i++) {
11. if (array[i] > array[maxIndex]) {
12. maxIndex = i;
13. }
14. }
15.
16. int temp = array[minIndex];
17. array[minIndex] = array[maxIndex];
18. array[maxIndex] = temp;
19. }复制代码
或者你也能够把相对独立的代码块封装成方法,这样写出的代码更为模块化。
1. void swapMinMaxBetter(int[] array) {
2. int minIndex = getMinIndex(array);
3. int maxIndex = getMaxIndex(array);
4. swap(array, minIndex, maxIndex);
5. }
6.
7. int getMinIndex(int[] array) { ... }
8. int getMaxIndex(int[] array) { ... }
9. void swap(int[] array, int m, int n) { ... }复制代码
虽然非模块化的代码也不算糟糕透顶,可是模块化的好处是易于测试,由于每一个组件均可以单独测试。随着代码愈来愈复杂,代码的模块化也越发重要,这将使代码更易维护和阅读。面试官想在面试中看到你能展现这些技能。
你的面试官要求你写代码来检查一个典型的井字棋是否有个赢家,并不意味着你必须要假定是一个3×3的棋盘。为何不把代码写得更为通用一些,实现成的棋盘呢?
把代码写得灵活、通用,也许意味着能够经过用变量替换硬编码值或者使用模板、泛型来解决问题。若是能够的话,应该把代码写得更为通用。
固然,凡事无绝对。若是一个解决方案对于通常状况而言显得太过复杂,而且不合时宜,那么实现简单预期的状况可能更好。
一个谨慎的程序员是不会对输入作任何假设的,而是会经过ASSERT
和if
语句验证输入。
一个例子就是以前把数字从进制(好比二进制或十六进制)表示转换成一个整数。
1. int convertFromBase(String number, int base) {
2. if (base < 2 || (base > 10 && base != 16)) return -1;
3. int value = 0;
4. for (int i = number.length() - 1; i >= 0; i--) {
5. int digit = digitToValue(number.charAt(i));
6. if (digit < 0 || digit >= base) {
7. return -1;
8. }
9. int exp = number.length() - 1 - i;
10. value += digit * Math.pow(base, exp);
11. }
12. return value;
13. }复制代码
在第2行,检查进制数是否有效(假设进制大于10时,除了16之外,没有标准的字符串表示)。在第6行,又作了另外一个错误检查以确保每一个数字都在容许范围内。
像这样的检查在生产代码中相当重要,也就是说,面试中一样重要。
不过,写这样的错误检查会很枯燥无味,还会浪费宝贵的面试时间。关键是,要向面试官指出你会写错误检查。若是错误检查不是一个简单的if
语句能解决的,最好给错误检查留有空间,告诉面试官等完成其他代码后还会返回来写错误检查。
面试题有时会让人不得要领,但这只是面试官的测试手段。直面挑战仍是知难而退?不畏艰险,奋勇向前,这一点相当重要。总而言之,切记面试不是一蹴而就的。遇到拦路虎本就在乎料之中。
还有一个加分项:表现出解决难题的满腔热情。
本文摘自《程序员面试金典(第6版)》