您的位置:首页 > 职场人生

一个经典编程面试题的“隐退”

2014-06-29 22:00 190 查看

问题

我把它叫做“分词”问题并解释如下:
给定一个输入的字符串和一个包含各种单词的字典,用空格将字符串分割成一系列字典中存在的单词。举个例子,如果输入字符串是“applepie”而字典中包含了所有的英文单词,那么我们应该得到返回值“apple pie”。
注意,我故意没有解释或者漏掉了一些细节,从而给候选人一个弄清楚问题的机会。 这里我举一些候选人可能会问的问题,以及我会如何回答
问:如果输入字符串本身是一个单词怎么办?
答:可以把它看作一个特殊情况。
问:我只用考虑分割成两个单词的情况吗?
答:不,但是可以从这种情况开始。
问:如果输入字符串无法被分割成单词怎么办?
答:返回null或者类似的东西。
问:有变位或者拼写错误怎么办?
答:只需要严格分割成字典中有的单词。
问:如果有多种分割可能性怎么办?
答:只需要返回任何一个正确的答案。
问:我在想将字典用前缀树,后缀树,Fibonacci堆实现…
答:你不用实现字典。只需要假设它已经被合理地实现了。
问:字典支持哪些操作?
答:字符串查询——这就是你所需要的全部
问:字典有多大?
答:假设它远远大于输入字符串,但足够装入内存。

观察求职者如何讨论这些问题,能帮助你了解求职者的沟通技巧和对细节的关注,以及求职者对数据结构和算法的基本理解。


简单解法

题目介绍的足够多了,我们接着看看解法。一些求职者从问题的简易版入手,即只考虑把字符串拆分成两个单词。我把它看作一个“傻瓜”解法,并且期望任何有竞争力的软件工程师可以给出任何等价于下面解法的代码。在我的解答中将使用Java实现。
1


2


3


4


5


6


7


8


9


10


11


12


13
String SegmentString(String input, Set<String> dict) {



 
int

len = input.length();



 
for

(

int

i =

1

; i < len; i++) {



 
String prefix = input.substring(

0

, i);



 
if

(dict.contains(prefix)) {



 
String suffix = input.substring(i, len);



 
if

(dict.contains(suffix)) {



 
return

prefix +

" "

+ suffix;



 
}



 
}



 
}



 
return

null

;



}

我面试过无法给出上面解法的求职者——其中一些甚至通过了Google的技术面。如同Jeff Atwood所说,傻瓜问题是避免面试官在那些根本不会编程的求职者身上浪费时间的好办法。


通用解法

当然,这个问题的精华在于它的一般情况,即输入字符串可以被分割成任意数量的单词。有很多方法可以解决这个问题,但是最直接的是递归回溯。这是一个典型的建立在上个解法上的版本:
1


2


3


4


5


6


7


8


9


10


11


12


13


14


15
String SegmentString(String input, Set<String> dict) {



 
if

(dict.contains(input))

return

input;



 
int

len = input.length();



 
for

(

int

i =

1

; i < len; i++) {



 
String prefix = input.substring(

0

, i);



 
if

(dict.contains(prefix)) {



 
String suffix = input.substring(i, len);



 
String segSuffix = SegmentString(suffix, dict);



 
if

(segSuffix !=

null

) {



 
return

prefix +

" "

+ segSuffix;



 
}



 
}



 
}



 
return

null

;



}

许多申请软件工程师职位的候选人无法在半小时内得到相当于上面解法的方法(比如一个用显式栈实现的算法)。我可以肯定他们很有竞争力并且很会写代码,但是我不会把他们安排到有关信息检索或者机器学习的职位上,尤其像那些开发大规模搜索功能的公司。


运行时间研究

但是等一下,问题不仅于此!当一个求职者走到了上面这一步,我会让他研究这个算法的最差运行时间,用字符串的长度n表示。我听过从O(n)到O(n!)的各种各样的回答。
我通常会提供以下提示:
考虑一个想象中的字典,它只包含”a”,”aa”,”aaa”,…,这样仅由字母”a”组成的单词。如果输入字符串是由一长串”a”和最末尾一个”b”组成会发生什么?
求职者最好能发现这样的话递归回溯将会寻找每一个可能的分割,于是问题就变成了举出单纯分割字符串的所有可能性。我把这个问题作为练习留给读者自己思考,答案是时间复杂度O(2^n)。


高效解法

如果求职者能走到这一步,我会问他是否可以做得更好。许多求职者意识到这是一个负载问题,更厉害的那些意识到可以用动态规划来完成。这是一个用了存储的解法:
1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21
Map<String, String> memoized;






String SegmentString(String input, Set<String> dict) {



 
if

(dict.contains(input))

return

input;



 
if

(memoized.containsKey(input) {



 
return

memoized.get(input);



 
}



 
int

len = input.length();



 
for

(

int

i =

1

; i < len; i++) {



 
String prefix = input.substring(

0

, i);



 
if

(dict.contains(prefix)) {



 
String suffix = input.substring(i, len);



 
String segSuffix = SegmentString(suffix, dict);



 
if

(segSuffix !=

null

) {



 
memoized.put(input,prefix +

" "

+ segSuffix);



 
return

prefix +

" "

+ segSuffix;



}



}



memoized.put(input,

null

);



return

null

;



}

同样的求职者需要进行时空分析。关键点是SegmentString方法只需要在输入字符串的后缀中执行,并且只有O(n)数量的后缀。我把这个作为练习留给读者,这个解法的时间复杂度为O(n^2)。


为什么我喜欢这个问题

有非常多的理由让我喜欢它。我说几点:

这是一个在现实的软件开发过程中会遇到的问题。我曾经为Endeca开发搜索词重写,这个问题会在拼写检查和同义词扩充时出现。


它不要求任何特殊的知识——仅仅是字符串、集合、表、递归和动态规划的简单应用。这些都是本科一二年级的基础内容。


写出这些代码并不容易,45分钟的时间会很紧凑,不论面试是电话进行或者用Collabedit这样的工具。


这个问题很有挑战性,但不是个故意难倒你的问题。它需要一些有条理的分析以及对基本工具的使用。


候选人在这个问题上的表现不是非对即错的。最糟糕的候选人甚至不能在45分钟内想出第一个解法。最好的候选人能在10分钟内实现带存储的解法,这使得你有机会问一些更有趣的问题,比如他们如何处理一个大到难以放在内存中的字典。多数候选人的表现在这两种之间。





“退休”愉快

不幸的是,所有好的东西都有尽头。我最近发现有人把这个题目放到了Glassdoor上。那里的解法没有我这篇讲的这么深入,而我认为像这样好的题目应该体面“善终”。
想出一个好的面试题很难,保守秘密同样很难。秘诀是保守更少的秘诀。一个理想的面试题应该尽量不涉及更高端的知识。我和我的同事们正在像这个方向努力。一旦我们有所进展,我自然会分享更多。
同时,我希望每一个经历过分词问题的人能够感激它带给我们的价值。没有完美的题目,同样一个人在一个面试题上的表现无法说明他在工作中的表现会怎样。尽管如此,这个题目仍然很棒,我知道很多人会怀念它。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: