编程语言应用

首页 » 常识 » 常识 » Google经典面试题解析
TUhjnbcbe - 2023/3/15 20:19:00
北京中科白癜风医院爱心分享会 http://m.39.net/baidianfeng/a_6169853.html

在这篇文章中,作者将介绍其过去在Google面试时采用的面试题,不过由于题目泄漏现在已经禁止在面试中使用了。然而,失之东隅收之桑榆,因为这些题目已被泄漏,所以笔者希望透过问题看本质,分享Google面试值得注意的细节,希望对大家有所裨益。

作者

AlexGolec译者

弯月责编

屠敏出品

CSDN

在深入问题之前,有一个令人振奋的消息:我离开了Google!我激动地宣布,我已经加入了Reddit,并在纽约市担任项目经理!

声明:虽然面试候选人是我的职责,但这篇文章仅代表我个人的观察、轶事和观点。请不要把本文当成Google、Alphabet、Reddit,或其他个人或组织的官方声明。

问题

想象一下,你在操作一个流行的搜索引擎,而且你在日志中看到了两则查询,比如说“奥巴马的支持率”和“奥巴马的流行度”。(如果我没记错的话,这些是数据库面试中实际使用的例子,虽然这个问题的日期有点过时了……)这两个查询是不同的字符串,但是我认为(相信你也会同意)从根本上说它们查找的都是同一个东西,在计数查询、显示结果等方面可以将这两个查询视作等价。那么,我们如何才能知道两个查询是同义词呢?

让我们用正式语言描述一下。假设你有两个输入。第一个是一个列表,其中每个元素都是成对的两个字符串,而且它们是同义词。第二个也是一个列表,每个元素都是一组成对的字符串,代表两个查询。

为了具体起见,我通过以下示例输入来说明:

SYNONYMS=[(rate,ratings),(approval,popularity),]QUERIES=[(obamaapprovalrate,obamapopularityratings),(obamaapprovalrates,obamapopularityratings),(obamaapprovalrate,popularityratingsobama)]

你的任务是输出一组布尔值,其中的每一个值说明相应的一对查询是否是同义词。

问题,问题

从表面上看,这是一个很简单的问题。然而,你越细想,就会觉得它越复杂。首先很明显,这个问题的定义并不完善。每个单词可以有多个同义词吗?单词的顺序有关系吗?同义词关系是否具有传递性,也就是说如果A与B是同义词,而B与C是同义词,那么是否意味着A与C也是同义词?多个单词组成的词语也是同义词吗?例如,“USA”与“UnitedStatesofAmerica”或“UnitedStates”是不是同义词?

优秀的候选人会立刻将这种模糊性当作机会,让自己脱颖而出。优秀的候选人会做的第一件事就是找出这类的歧义,并设法解决它们。如何做到这一点因人而异:有些人会到白板前设法手动解决具体的问题;有些人则看一眼立即就能看出其中的猫腻。不管怎样,尽早发现这些问题至关重要。

我非常重视这个问题的“问题理解”阶段。我喜欢将软件工程称为分形学科,这意味着它与分形学有相似之处,即放大问题就可以显现出额外的复杂性。就在你以为你理解了某个问题的时候,走近一看才发现你忽略了一个微妙之处,或者可以改进的实施细节,或者找到一种新的看待这个问题的方法从而洞悉更多细节。

工程师的能力在很大程度上取决于他们对问题的理解程度。将一个模糊的问题陈述转换成一组详细的需求是这个过程的第一步,有目的地定义问题是我评估候选人处理新情况能力的依据。

顺便说一句,还有一些不重要的问题,例如“是否需要考虑大小写”,这些问题即使候选人不知道也不会影响核心算法问题。对于这些问题,一般情况下我会给出最简单的答案(就这个例子而言,我会说“假设所有内容都已预处理为小写了”)。

第1部分:(并非)简单的例子

每当候选人遇到这些问题时,他们总是会问我答案,而我总是会从最简单的情况开始:单词可以有多个同义词,顺序很重要,同义词不可传递,而且同义词只能从一个单词映射到另一个。所以放到搜索引擎中则功能非常有限,但它的细微之处会给面试带来很多问题。

这个问题的解决方案可以高度概括为:将查询分解成单词(用空格分割就可以),并比较相应成对的单词,看看它们是否完全相同或者是同义词。如下图所示:

实现大致如下:

defsynonym_queries(synonym_words,queries):synonym_words:iterableofpairsofstringsrepresentingsynonymouswordsqueries:iterableofpairsofstringsrepresentingqueriestobetestedforsynonymous-nessoutput=[]forq1,q2inqueries:q1,q2=q1.split(),q2.split()iflen(q1)!=len(q2):output.append(False)continueresult=Trueforiinrange(len(q1)):w1,w2=q1,q2ifw1==w2:continueelifwords_are_synonyms(w1,w2):continueresult=Falsebreakoutput.append(result)returnoutput

请注意:这里我故意没有定义words_are_synonyms

很简单,对不对?从算法上讲,这非常简单。没有动态规划,没有递归,没有棘手的数据结构等等。只是非常简单的标准库操作,以及线性时间算法,对吧?

你可能会这么想,但是这里面比你第一眼看到的更微妙。到目前为止,这个简单的算法中最棘手的部分是同义词比较。虽然易于理解和描述,但同义词比较这部分有可能会出很多错。下面我来介绍一下我看到的一些常见的问题。

首先声明,在我看来候选人不会因为这些错误而遭遇面试失败;如果候选人做出来的实现有错,那么我会指出来,他们会调整他们的解决方案,然后面试继续。但是,面试是一个分秒必争的过程。犯错,发现错误,改正错误,这些行为都是意料之中的,但是因此而浪费掉的时间本可以用来干别的,比如找到更优的解决方案等。不犯错的候选人很少,但是犯错少的候选人就可以做得更好,因为他们花费在清理错误上的时间更少。

这就是我喜欢这道题目的原因:文章中的题目需要在灵光闪现之际找到一个算法,然后再找到一个简单的实现。这道题目与之不同,它需要在正确的方向上一步步前行。每一步都代表着一个很小的障碍,候选人可以优雅地跳过去,或者被绊倒再站起来。优秀的候选人会利用他们的经验和直觉来避免这些小陷阱,并找到更加详实和正确的解决方案,而实力比较弱的人会浪费时间和精力去处理错误,而且通常最后只会留下错误累累的代码。

每次面试我都会看到有人优雅地跳过去了,而有人却摔得鼻青脸肿,但在此我只想举几个例子说明常见的几个小错误。

意外的运行时杀手

首先,有些候选人会通过简单地遍历同义词列表来实现同义词的检测:

...elif(w1,w2)insynonym_words:continue...

从表面上看,这似乎很合理。但仔细观察,你就会发现这是一个非常非常糟糕的主意。我想跟那些不了解Python的人解释一下:关键字in是contains方法的语法糖,适用于所有标准的Python容器。这里的问题在于synonym_words是一个列表,它通过线性搜索实现了关键字in。Python用户特别容易受到这种错误的影响,因为这种语言会隐藏类型,但C++和Java用户偶尔也会犯同样的错误。

在我的整个职业生涯中,编写这类线性搜索代码的次数屈指可数,而且每次涉及的列表都不会超过二十多个元素,即便如此,我还是会写一大篇注释告诉读者为什么我选择了这种看似不太理想的方法。我怀疑有些候选人在这里使用线性搜索的原因是因为他们对Python标准库的了解不够,他们不知道如何在列表上实现关键字in。这是很容易犯的一个错误,虽然这并不致命,但是你对选择的语言不够熟练似乎也不太好看。

至于实际的建议嘛,其实很容易避免这种错误。首先,在你使用python这种无类型的语言时,永远不要忘记对象的类型!其次,请记住,如果你对列表使用关键字in,那么就会形成线性搜索。除非你可以保证这个列表始终非常小,否则它就会成为性能杀手。

通常,提醒候选人这个输入结构是一个列表就可以让他们反应过来。在我给出提示后就有好戏看了。优秀的候选人会立即想到以某种方式预处理同义词,这是一个不错的开端。然而,这种方法也并非没有陷阱......

使用正确的数据结构

从上面的代码可以看出,为了在线性时间内实现这个算法,我们需要一个常数时间的同义词查找。而且在每次常数时间的查找后面都应该有一个hashmap或hashset。

我感兴趣的不是候选人会从这两个中选择哪一个,而是他们会在里面存什么。(顺便说一句,永远不要使用返回True或False的dict/hashmap。这叫做集合。)大多数的候选人都会选择某种dict/hashmap。我最常见到的错误是一种潜意识的假设,即每个单词最多只有一个同义词:

...synonyms={}forw1,w2insynonym_words:synonyms[w1]=w2...elifsynonyms[w1]==w2:continue

我并不会因为这个错误而惩罚候选人。这个示例的输入是有意设计成让人想不起单词可以有多个同义词,而有些候选人根本想不到这种边界情况。在我指出这个错误后,大多数人都会快速改正。优秀的候选人会在早期注意到这个问题,从而避免这种麻烦,但通常这不会造成大量的时间流逝。

一个稍微严重的问题是,没有意识到同义词关系是双向的。你可能注意到上述代码会这么做。然而,改正这个问题可能会出错。请考虑如下实现这个属性的方法:

...synonyms=defaultdict(set)forw1,w2insynonym_words:synonyms[w1].append(w2)synonyms[w2].append(w1)...elifw2insynonyms.get(w1,tuple()):continue

如果你可以不消耗额外的内存只需执行两次检查,那么为什么要用两个插入来消耗双倍内存呢?

...synonyms=defaultdict(set)forw1,w2insynonym_words:synonyms[w1].append(w2)...elif(w2insynonyms.get(w1,tuple())orw1insynonyms.get(w2,tuple())):continue

提示:始终要问问你自己是否可以减少工作量!事后看来,对查找进行排列明显是一种节省时间的方法,但是使用非最优的实现则表明候选人没有考虑寻找优化的方法。再次重申,我可以给出提示,但是无需我提示不是更好吗?

排序?

有些很聪明的候选人认为可以对同义词列表进行排序,然后使用折半查找法来检查两个单词是否是同义词。实际上这种方法的主要优点在于,除了输入的同义词列表外,不占用任何额外的空间(假定可以修改输入列表)。

不幸的是,时间复杂度并不是很大:对同义词列表进行排序需要花费的时间为Nlog(N),而查找每对同义词的时间为log(N),而上述预处理解决方案是线性的,接下来才是查找的常数时间。另外,我并不想让候选人在白板上实现排序和折半查找法,因为(1)排序算法众所周知,所以我知道候选人可能只是做机械的重复;而且(2)想要写正确这些算法其实还是很有难度,通常即使最优秀的候选人偶尔也会犯错,难道你能说他们的编程能力有问题吗?

每当候选人提供这种解决方案时,我都会询问运行时的复杂性,并问他们有没有更好的方法。顺便提一句:如果面试官问你有没有更好的方法,那么绝大多数情况下的答案都是“是”。如果我问过你这个问题,那么答案肯定是“是”。

最后的解决方案

希望到这里候选人已经得出了正确且最优的结果。以下是这道题目线性时间线性空间的实现:

defsynonym_queries(synonym_words,queries):synonym_words:iterableofpairsofstringsrepresentingsynonymouswordsqueries:iterableofpairsofstringsrepresentingqueriestobetestedforsynonymous-nesssynonyms=defaultdict(set)forw1,w2insynonym_words:synonyms[w1].add(w2)output=[]forq1,q2inqueries:q1,q2=q1.split(),q2.split()iflen(q1)!=len(q2):output.append(False)continueresult=Trueforiinrange(len(q1)):w1,w2=q1,q2ifw1==w2:continueelif((w1insynonymsandw2insynonyms[w1])or(w2insynonymsandw1insynonyms[w2])):continueresult=Falsebreakoutput.append(result)returnoutput

以下是一些简要说明:

注意dict.get()的使用。你可以采用“先检查key是否在dict中再获取”的实现,但那样你就失去了展示你对标准库的了解的机会。我个人并不太喜欢用了很多continue的代码,而且有些编程风格指南禁止或不建议这么使用。其实,我最初的代码中有一个bug——查询长度检查后面省略了continue。这个bug不是很糟糕,但是要知道它很容易出错。

第2部分:加大难度!

在面试优秀的候选人时,我常常发现最后还剩下10-15分钟的时间。幸运的是,我可以提出很多后续的问题,但是我们不太可能在那段时间内编写很多代码。尽管在一天结束后,我觉得自己没必要那么做,但我想了解候选人两方面的能力:他们能够设计算法吗?还有他们能够写代码吗?我文章中的问题首先回答了算法设计的问题,然后还可以检查代码,而本文中的这道题目得到答案的顺序正好相反。

等到候选人完成了这道题目的第一部分后,他们就解决了(这个非常有难度的)编程问题。这时,我可以很自信地说他们具备设计基本算法的能力,还可以将他们的想法转化为代码,而且他们还很熟悉自己喜欢的语言和标准库。接下来这个问题就变得更加有趣了,因为编程要求已经可以了,我们可以深入研究算法部分了。

为此,让我们回到第一部分的基本假设:单词的顺序很重要,同义词关系没有传递性,同义词不能包含多个单词。随着面试的进行,我会改变这些约束,而且在这个编程后的阶段里,我和候选人可以只讨论纯粹的算法。我会通过一些代码示例来说明我的观点,但在实际面试中,我会使用纯粹的算法术语进行讨论。

在深入说明之前,我想说从我的期望值来看后面的面试基本上都属于“加分项”。我个人对这个问题的处理方法是,挑选第一部分考察完全“合格”的候选人,然后通过下一个环节的考察从“合格”的候选人挑选“强力”的候选人。“合格”的候选人已经很厉害了,代表了“我相信这个人可以很好地胜任工作”,而“强力”的候选人则表示“我相信这个人非常优秀,聘用他们可以为公司带来巨大的利益。”

传递性:朴素的方法

我想讨论的第一个问题是有关传递性,也就是说如果单词A与B是同义词,而单词B与C是同义词,那么单词A与C也是同义词。反应灵敏的候选人很快会意识到他们可以调整之前的解决方案来解决这个问题,因为他们仍然觉得应该检查简单的一对单词是否是同义词,而有的人则认为之前的算法的核心逻辑已经没用了。

那么究竟我们该怎么做呢?一种常见的方法是根据传递关系为每个单词维护一组完整的同义词。每当我们向同义词集合中插入一个单词时,同时也把它插入到该集合当前所有单词相应的集合中:

synonyms=defaultdict(set)forw1,w2insynonym_words:forwinsynonyms[w1]:synonyms[w].add(w2)synonyms[w1].add(w2)forwinsynonyms[w2]:synonyms[w].add(w1)synonyms[w2].add(w1)

请注意,通过以上代码我们已经深入到我允许候选人选用的这个解决方案中了。

这个解决方案很有效,但它远非最佳解决方案。想知道为什么吗?让我们来考虑一下这个解决方案的空间复杂性。每当添加一个同义词,我们不仅要添加到起始单词的集合,还要添加到该单词的所有同义词的集合。如果该单词有一个同义词,那么就需要添加一条数据。如果该单词有50个同义词,那么我就需要添加50条数据。如下图所示:

请注意,我们已经从3个键和6个数据项扩展到了4个键和12个数据项。如果一个单词有50个同义词,那么就需要50个键和将近个数据项。表示一个单词所需的空间与其同义词集的大小呈二次方式增长,这是巨大的浪费。

还有其他解决方案,但考虑到篇幅有限,我就不在此赘述了。其中最有趣的一种方法是使用同义词的数据结构来构造有向图,然后使用广度优先搜索来查找两个单词之间是否存在路径。这是一个很好的解决方案,但查找就变成了单词同义词集大小的线性。由于每个查询我们都需要执行多次查找,所以这个方法并不是最优解决方案。

传递性:使用不相交集

事实证明,我们可以通过使用名为“不相交集”的数据结构在常数时间内查找同义词关系。这种结构称为集合,但它提供的功能与大多数人想象中的单词“集合”有所不同。

常见的集合结构(hashset,treeset)是一个容器,允许你快速查找集合中是否包含某个对象。不相交集(disjointset)解决的是另一个不同的问题:它并不

1
查看完整版本: Google经典面试题解析