面試程式設計師很困難。Jeff Atwood 抱怨找一個會寫程式碼的候選人是如此艱難。在技術媒體釋出的那些“最佳”面試題中,很少有能讓我提起興趣的——儘管我很喜歡IKEA的這個面試題。Codility和 Interview Street這樣的創業公司從這個具有挑戰性的課題中看到了機會。與此同時,Diego Basch 呼籲我們停止逼迫求職者進行白板程式設計。
對此我沒有什麼更好的建議。我同意IQ測試和刁難人的問題非常糟糕。在最好的情況下,它僅僅能測試候選人的一項素質;在最壞的情況下,它完全不能說明候選人是因為曾經遇到過相同的問題,還是靠著自己的能力找到了解決方法。程式設計題對於一個一整天的工作內容就是寫程式碼的人來說是更好的面試方法,但是傳統的方法,不論是電話還是面對面交流,都不是最優的測試程式設計能力的方法。同樣,人們也不是很清楚程式設計題應該是以怎樣的形式呈現——直接解決問題,還是僅僅把一個演算法翻譯成可執行的程式碼?
面對著如此多的挑戰,我想到了一個為我以及其他在Endeca、 Google 和LinkedIn工作的同僚們服務了多年的面試題。我帶著沉重的心情解這個題,原因我會在結尾中解釋。但是首先讓我描述下這個問題,並且解釋為什麼它如此有效。
問題
我把它叫做“分詞”問題並解釋如下:
給定一個輸入的字串和一個包含各種單詞的字典,用空格將字串分割成一系列字典中存在的單詞。舉個例子,如果輸入字串是“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上。那裡的解法沒有我這篇講的這麼深入,而我認為像這樣好的題目應該體面“善終”。
想出一個好的面試題很難,保守祕密同樣很難。祕訣是保守更少的祕訣。一個理想的面試題應該儘量不涉及更高階的知識。我和我的同事們正在像這個方向努力。一旦我們有所進展,我自然會分享更多。
同時,我希望每一個經歷過分詞問題的人能夠感激它帶給我們的價值。沒有完美的題目,同樣一個人在一個面試題上的表現無法說明他在工作中的表現會怎樣。儘管如此,這個題目仍然很棒,我知道很多人會懷念它。