刷道谷歌泄漏的面试题:面试官想从中考察你什么?
這是“谷歌面試題解析”系列的又一篇文章。在這一系列文章中,我介紹了谷歌面試當中經(jīng)常用到的一些面試題,不過這些面試題已經(jīng)被泄露,并禁止在面試中使用。不過,我的損失就是你的收獲,因為它們被泄露了,我就可以把它們寫下來,供你參考。上篇:
一道泄露并遭禁用的谷歌面試題,背后玄機全解析
在介紹新的面試題之前,我先宣布一個令人興奮的消息:我已經(jīng)離開谷歌了!我現(xiàn)在是 Reddit 的工程經(jīng)理!不過,我還是會繼續(xù)發(fā)表這一系列的文章,請繼續(xù)關(guān)注。
免責聲明:雖然面試候選人是我的工作職責之一,但這篇文章僅代表我的個人觀察、個人經(jīng)歷和個人觀點。如果有任何錯誤,請不要將它們歸咎于谷歌、Alphabet、Reddit 或任何其他個人或組織。
問題描述
假設(shè)你在運營一個搜索引擎,在搜索引擎日志中可以看到兩個搜索關(guān)鍵詞,比如“obama approval ratings(奧巴馬支持率)”和“obama popularity rate(奧巴馬受歡迎程度)”。雖然這兩個搜索字符串不一樣,但基本上是在搜同一個東西,在計算搜索和顯示結(jié)果時應(yīng)該被認為是等價的。那么我們該如何判斷這兩個搜索詞是不是同義的呢?
我們先將這個問題泛化。假設(shè)你有兩個輸入,一個是字符串對列表,其中每個字符串對是兩個同義詞。另一個也是字符串對列表,其中每個字符串對是兩個搜索關(guān)鍵詞。
下面是輸入示例:
SYNONYMS = [ ('rate', 'ratings'), ('approval', 'popularity'), ] QUERIES = [ ('obama approval rate', 'obama popularity ratings'), ('obama approval rates', 'obama popularity ratings'), ('obama approval rate', 'popularity ratings obama') ]你的任務(wù)是輸出一個布爾值列表,其中每個布爾值表明相應(yīng)的搜索關(guān)鍵詞對是不是同義的。
理清問題
從表面上看,這是一個很簡單的問題。但是,越是細想,它就越復(fù)雜。首先,這個問題的定義不是很明確。單詞可以有多個同義詞嗎?單詞的順序重要嗎?同義詞之間是否具有傳遞性,例如,如果 A 與 B 同義,B 與 C 同義,那么 A 是否也與 C 同義?同義詞可以跨多個單詞嗎,例如“USA”與“United States of America”同義,還是與“United States”同義?
這種模棱兩可給了優(yōu)秀候選人一個脫穎而出的機會。好的候選人會先找出這些含糊不清的地方,并試圖解決它們。他們的做法因人而異:有些人會走到白板前,試圖手動解決一些特定的情況,而另一些人一看到問題,就會立即發(fā)現(xiàn)其中的“陷阱”。無論如何,關(guān)鍵的是要盡早發(fā)現(xiàn)這些問題。
我非??粗剡@個“理解問題”階段。我喜歡將軟件工程稱為分形學科,這意味著它與分形具有相同的特性,通過放大問題來顯示額外的復(fù)雜性。當你認為你理解了一個問題,仔細一看,就會發(fā)現(xiàn)其實忽略了一些細微之處,或者一些可以改進的實現(xiàn)細節(jié),或者有其他新的方法可以用來分析這個問題并揭示出額外的見解。
工程師的能力在很大程度上取決于他們對問題理解的深度。將一個模糊的問題陳述轉(zhuǎn)化為一組詳細的需求是這個過程的第零步,像這樣故意向候選人提出不明確問題的做法可以幫助面試官評估候選人應(yīng)對意外情況的能力。
第 1 部分:簡單的情況
不管候選人是如何進入到這一步的,他們都不可避免地要我回答他們提出的疑問,我總是從最簡單的情況開始:單詞可以有多個同義詞、單詞的順序重要、同義詞不具有傳遞性、同義詞只能從一個單詞映射到另一個。盡管這樣的搜索引擎功能非常有限,但對于一道有趣的面試題來說,已經(jīng)足夠了。
解決方案大概是這樣的:將搜索關(guān)鍵詞分解為單詞(按照空格進行拆分就可以了),并比較相應(yīng)的單詞對,看看它們是否相同或者同義。它們看起來像這樣:
實現(xiàn)代碼大概是這樣的:
def synonym_queries(synonym_words, queries): ? ''' ? synonym_words: iterable of pairs of strings representing synonymous words ? queries: iterable of pairs of strings representing queries to be tested for?synonymous-ness ? ''' ? output = [] ? for q1, q2 in queries: ? ? ? q1, q2 = q1.split(), q2.split() ? ? ? if len(q1) != len(q2): ? ? ? ? ? output.append(False) ? ? ? ? ? continue ? ? ? result = True ? ? ? for i in range(len(q1)): ? ? ? ? ? w1, w2 = q1[i], q2[i] ? ? ? ? ? if w1 == w2: ? ? ? ? ? ? ? continue ? ? ? ? ? elif words_are_synonyms(w1, w2): ? ? ? ? ? ? ? continue ? ? ? ? ? result = False ? ? ? ? ? break ? ? ? output.append(result) ? return output很簡單,對嗎?從算法來看,確實很簡單。沒有動態(tài)規(guī)劃,沒有遞歸,沒有特別的數(shù)據(jù)結(jié)構(gòu),只要使用標準庫操作和線性時間算法,對吧?
你可能會想,但這里的微妙之處比你第一眼看到的要多。到目前為止,這個算法最復(fù)雜的部分是同義詞比較。雖然易于理解和描述,但同義詞比較很有可能會出錯。
需要明確的是,在我看來,這些錯誤都不能說明候選人是不合格的。如果候選人給出了包含錯誤的實現(xiàn),我會直接指出來,他們會調(diào)整他們的解決方案。但是,面試首先是一場與時間賽跑的競賽。犯錯誤、找出錯誤和糾正錯誤是意料之中的事,但這樣會消耗原本可以花在其他地方的時間,比如提出更優(yōu)的解決方案。很少有候選人不犯錯誤,但錯誤犯得少的候選人會取得更大的進步,因為他們花在糾錯上的時間更少。
這也是為什么我會喜歡這個面試題:在解決騎士撥號器問題時,需要算法的靈光一現(xiàn),然后給出一個簡單的實現(xiàn),而這個問題需要多個漸進式的小步驟,朝著正確的方向逐漸接近答案。每一步都代表了一個小障礙,候選人可以優(yōu)雅地跳過,也可能會被絆倒,然后重新站起來。優(yōu)秀的候選人利用他們的經(jīng)驗和直覺來避免這些小陷阱,他們會得到一個更加完善和正確的解決方案,而較弱的候選人則會在錯誤上浪費時間和精力,通常會給出包含錯誤的代碼。
每次面試都會出現(xiàn)上述的狀況,這里列出了我看到的更為常見的一小部分。
?意外運行時殺手
首先,一些候選人通過遍歷同義詞列表來實現(xiàn)同義詞檢查:
... elif (w1, w2) in synonym_words: continue ...從表面上看,這樣似乎是合理的。但仔細一看,就會發(fā)現(xiàn)這是一個非常糟糕的主意。如果你不了解 Python,我可以解釋一下:in 關(guān)鍵字是 contains 方法的語法糖,適用于所有標準的 Python 容器。這里的問題在于,synonym_words 是一個列表,通過線性搜索來實現(xiàn) in 關(guān)鍵字。Python 用戶很容易犯這個錯誤,因為 Python 隱藏了類型,不過 C++ 和 Java 用戶偶爾也會犯類似的錯誤。
在我的整個職業(yè)生涯中,我只寫過幾次使用線性搜索的代碼,而且每次都只涉及不到 24 個元素的列表,即使是這樣,我也寫了很長的注釋,讓閱讀代碼的人知道我為什么選擇這種似乎不太理想的方法。我認為一些候選人之所以使用它,是因為他們不太了解 Python 標準庫,不知道在列表上使用 in 關(guān)鍵字是如何實現(xiàn)的。這是一個很容易犯的錯誤,不過這也不是什么大不了事,只是你選擇了自己不熟悉的語言,對你不是很有利。
實際上,這是一個很容易就可以避免的錯誤。首先,永遠不要忘記對象的類型,即使你使用的是 Python 這種非類型化的語言!其次,請記住,在列表上使用 in 關(guān)鍵字是一種線性搜索。除非這個列表非常小,否則它將成為性能殺手。
提醒一下候選人,輸入的數(shù)據(jù)結(jié)構(gòu)是列表,這通常就足以讓他們“醒悟”。好的候選人會立即想辦法對同義詞進行預(yù)處理,這是一個好的開始。然而,這種方法并非沒有缺陷……
?使用正確的數(shù)據(jù)結(jié)構(gòu)
從上面的代碼可以看出,為了在線性時間內(nèi)實現(xiàn)這個算法,我們需要一種常量時間的同義詞查找方法,而常量時間查找需要用到 hashmap 或 hashset。
我感興趣的不是候選人會選擇使用哪個,而是他們會把什么東西放在里面。大多數(shù)候選人會選擇某種形式的 dict/hashmap。我看到的最常見的錯誤是候選人潛意識里認為每個單詞最多只能有一個同義詞:
... synonyms = {} for w1, w2 in synonym_words: synonyms[w1] = w2 ... elif synonyms[w1] == w2: continue我不會因為候選人犯了這個錯誤而懲罰他們。示例中給出的輸入是為了不讓人想起單詞可以有多個同義詞,一些候選人根本沒有遇到過這種情況。在我指出這個錯誤之后,他們迅速做出糾正。好的候選人會及早發(fā)現(xiàn)問題,從而避免麻煩,不過這也不會浪費太多時間。
一個稍微嚴重一點的問題是候選人意識不到同義詞關(guān)系是雙向的。然而,糾正這一點很容易出錯。請看下面這個糾正方法:
... synonyms = defaultdict(set) for w1, w2 in synonym_words: synonyms[w1].append(w2) synonyms[w2].append(w1) ... elif w2 in synonyms.get(w1, tuple()): continue為什么要執(zhí)行兩次插入并使用雙倍的內(nèi)存?其實完全可以在不使用額外內(nèi)存的情況下進行兩次檢查:
... synonyms = defaultdict(set) for w1, w2 in synonym_words: synonyms[w1].append(w2) ... elif (w2 in synonyms.get(w1, tuple()) or ? w1 in synonyms.get(w2, tuple())): continue結(jié)論是:總是問自己是否可以少做點工作!事后看來,排列查找顯然是一種可以節(jié)省時間的方法,但使用次優(yōu)實現(xiàn)說明候選人沒有想要尋找優(yōu)化方法。我很樂意給他們提示,但如果不需要我給提示會更好。
?排序?
一些比較聰明的候選人會考慮對同義詞列表進行排序,然后使用二分查找來確定兩個單詞是否同義。這種方法的主要優(yōu)點是除了輸入同義詞列表之外不需要任何額外的空間(假設(shè)可以修改輸入列表)。
可惜的是,時間復(fù)雜度還不夠好:排序同義詞列表需要 Nlog(N) 的時間復(fù)雜度,然后查找每個同義詞對需要 log(N) 的時間復(fù)雜度,而前面描述的預(yù)處理是線性的,在訪問時使用了常量時間。另外,候選人在白板上實現(xiàn)排序和二分查找在我看來是禁忌,因為排序算法已經(jīng)是眾所周知的東西,而且這些算法非常難搞對,通常即使是最優(yōu)秀的候選人也會犯錯誤,而這些錯誤并不能告訴我他們的編程能力究竟是怎樣的。
每當有候選人提供這樣的解決方案,我就會問他們運行時間復(fù)雜度,以及是否可以做得更好。順便說一句:如果面試官問你是否可以做得更好,大多數(shù)時候答案都是“是”。
?最后的解決方案
到了這個時候,候選人應(yīng)該已經(jīng)能夠給出最佳的解決方案了。下面是線性時間和線性空間的算法實現(xiàn):
def synonym_queries(synonym_words, queries): ? ''' ? synonym_words: iterable of pairs of strings representing synonymous words ? queries: iterable of pairs of strings representing queries to be tested for?synonymous-ness ? ''' ? synonyms = defaultdict(set) ? for w1, w2 in synonym_words: ? ? ? synonyms[w1].add(w2) ? output = [] ? for q1, q2 in queries: ? ? ? q1, q2 = q1.split(), q2.split() ? ? ? if len(q1) != len(q2): ? ? ? ? ? output.append(False) ? ? ? ? ? continue ? ? ? result = True ? ? ? for i in range(len(q1)): ? ? ? ? ? w1, w2 = q1[i], q2[i] ? ? ? ? ? if w1 == w2: ? ? ? ? ? ? ? continue ? ? ? ? ? elif ((w1 in synonyms and w2 in synonyms[w1]) or (w2 in synonyms and w1 in synonyms[w2])): ? ? ? ? ? ? ? continue ? ? ? ? ? result = False ? ? ? ? ? break ? ? ? output.append(result) ? return output一些注意點:
在使用 dict.get() 時要注意。你可能是想“檢查 dict 中是否存在某個鍵,然后獲取它”,但這樣有點混亂,這也表明了你對標準庫的了解情況。
我個人并不喜歡在代碼中大量使用 continue,一些編碼指南也建議不要這么做。
第 2 部分:加大難度
遇到優(yōu)秀的候選人,我通常還會剩下十到十五分鐘時間。所幸的是,我可以繼續(xù)問很多其他問題,但我們不太可能在這段時間內(nèi)寫很多代碼。但不管怎樣,我認為也沒必要寫太多代碼。我想知道關(guān)于候選人的兩件事是:他們可以設(shè)計算法嗎?他們可以寫代碼嗎?騎士撥號器問題需要先回答算法設(shè)計問題,然后再寫代碼,而這個問題恰好反過來。
當候選人完成這個問題的第一部分時,他們實際上已經(jīng)解決了編碼問題。從這一點上看,我可以自信地認為他們有設(shè)計基本算法并將想法轉(zhuǎn)化為代碼的能力,并且他們對自己喜歡的編程語言和標準庫也一定的了解。既然編碼方面沒有問題,那么我們就可以深入研究算法了。
為此,讓我們回到第一部分的基本假設(shè):單詞順序很重要、同義詞關(guān)系不具備傳遞性、不能有多個同義詞。隨著面試的進行,我改變了這些約束條件,我和候選人進行了純粹的算法討論。在這里,我將通過代碼示例來說明我的觀點,但在實際的面試中,我是通過純粹的算法術(shù)語和候選人進行討論的。
?傳遞性:初級方法
我想放寬的第一個約束是關(guān)于傳遞性的約束,也就是說,如果單詞 A 和 B 是同義的,而且單詞 B 和 C 也是同義的,那么單詞 A 和 C 就是同義的。敏銳的候選人很快就會意識到,他們需要調(diào)整之前的解決方案,因為約束的放寬導(dǎo)致之前算法的核心邏輯無效。
那么我們該怎么做呢?一種常見的方法是基于傳遞關(guān)系為每個單詞維護一組完整的同義詞。每次在同義詞集合中插入一個單詞時,也會將它添加到集合中所有單詞的同義詞集合中:
synonyms = defaultdict(set) for w1, w2 in synonym_words: ? for w in synonyms[w1]: ? ? ? synonyms[w].add(w2) ? synonyms[w1].add(w2) ? for w in synonyms[w2]: ? ? ? synonyms[w].add(w1) ? synonyms[w2].add(w1)這個方法是有效的,但并不是最好的??梢钥匆幌逻@個解決方案的空間復(fù)雜度。每次我們添加同義詞時,不僅要添加到初始單詞的同義詞集合中,還要添加到這個單詞所有同義詞的集合中。如果它與一個單詞同義,我們必須添加一個條目。如果它與 50 個單詞同義,我們必須再添加 50 個條目。如圖所示,它看起來像這樣:
請注意,我們已經(jīng)從 3 個鍵和 6 個條目變成了 4 個鍵和 12 個條目。一個包含 50 個同義詞的單詞將需要 50 個鍵和近 2500 個條目。表示一個單詞所需的空間與同義詞數(shù)量的大小呈二次方式增長,這非常浪費空間。
還有其他的解決方案,但我們關(guān)注的是空間,所以我不打算深入介紹它們。最有意思的一個解決方案是使用同義詞數(shù)據(jù)結(jié)構(gòu)來構(gòu)造有向圖,然后使用廣度優(yōu)先搜索來查找兩個單詞之間是否存在路徑。這是一個很好的解決方案,但查找時間復(fù)雜度是線性的。因為每次查詢需要進行多次查找,所以這個解決方案不是最優(yōu)的。
?傳遞性:使用并查集
事實證明,我們可以使用一種稱為并查集的數(shù)據(jù)結(jié)構(gòu),在(幾乎)恒定的時間內(nèi)查找同義詞關(guān)系。這種數(shù)據(jù)結(jié)構(gòu)是一種集合,但它提供的功能與大多數(shù)人認為的“集合”有些不一樣的地方。
通常的集合(如 hashset、treeset)是一種容器對象,可以讓你快速地知道一個對象是否存在集合中。而并查集解決了一個非常不一樣的問題:它可以讓你知道兩個對象是否屬于同一集合。更重要的是,它的時間復(fù)雜度是 O(a(n)),其中 a(n) 是 Ackerman 函數(shù)的逆。除非你上過高級算法課程,否則不知道這個函數(shù)也是情有可原的。對于所有合理的輸入,它幾乎都是常數(shù)時間。
這個算法的過程如下所述。集合通過樹來表示,每個元素都有一個父元素。因為每棵樹都有一個根元素(這個元素的父元素就是它自己),所以我們可以通過跟蹤兩個元素的父元素來確定它們是否屬于同一個集合。我們找到每個元素的根元素,如果這兩個根元素是同一個元素,說明這兩個元素屬于相同的集合。合并兩個集合也很簡單:我們只需要找到根元素,并讓其中一個成為另一個的根。
到目前為止,一切都很好,但在性能方面還沒看到有什么特別的地方。這種結(jié)構(gòu)的精妙之處在于可以進行壓實。假設(shè)你有這樣的一棵樹:
假設(shè)你想知道“speedy”和“hurry”是否是同義詞。從每個節(jié)點開始,遍歷父節(jié)點關(guān)系,發(fā)現(xiàn)它們的根節(jié)點都是“fast”,因此它們是同義詞。再假設(shè)你想知道“speedy”和“swift”是否是同義詞。你將再次從每個節(jié)點開始,一直向上遍歷,直到到達“fast”。但這次你可能會注意到,從“speedy”開始的遍歷重復(fù)了。“你能避免重復(fù)的遍歷嗎?”
事實證明,可以避免重復(fù)遍歷。因為這棵樹中的每個元素都注定要到達“fast”,所以與其多次遍歷樹,不如直接更改每個元素的父元素,直到“fast”,這樣可以幫我們省下很多工作。這個過程被稱為壓實,在一個并查集中,它發(fā)生在尋根操作過程中。例如,在我們確定“speedy”和“hurry”是同義詞之后,上面的樹將變成這樣:
“speedy”和“fast”路徑上的每個單詞的父元素都被更新了,“hasty”到“fast”之間的路徑也是如此。
現(xiàn)在,所有后續(xù)的訪問都將在常量時間內(nèi)完成,因為樹中的每個節(jié)點都指向“fast”。分析這個結(jié)構(gòu)的時間復(fù)雜度并不容易:它不是常數(shù),因為它取決于樹的深度,但也不會比常數(shù)差太多,我們姑且認為是常數(shù)時間。
下面是并查集的實現(xiàn),它為我們提供了解決這個問題所需的功能:
class DisjointSet(object): ? def __init__(self): ? ? ? self.parents = {} ? def get_root(self, w): ? ? ? words_traversed = [] ? ? ? while self.parents[w] != w: ? ? ? ? ? words_traversed.append(w) ? ? ? ? ? w = self.parents[w] ? ? ? for word in words_traversed: ? ? ? ? ? self.parents[word] = w ? ? ? return w ? def add_synonyms(self, w1, w2): ? ? ? if w1 not in self.parents: ? ? ? ? ? self.parents[w1] = w1 ? ? ? if w2 not in self.parents: ? ? ? ? ? self.parents[w2] = w2 ? ? ? w1_root = self.get_root(w1) ? ? ? w2_root = self.get_root(w2) ? ? ? if w1_root < w2_root: ? ? ? ? ? w1_root, w2_root = w2_root, w1_root ? ? ? self.parents[w2_root] = w1_root ? def are_synonymous(self, w1, w2): ? ? ? return self.get_root(w1) == self.get_root(w2)通過使用這種結(jié)構(gòu),我們可以預(yù)處理同義詞,并在線性時間內(nèi)解決這個問題。
評估和說明
到了這個時候,我們已經(jīng)達到了 40 到 45 分鐘的面試極限。如果候選人能夠完成算法介紹,并在描述(不是實現(xiàn))并查集解決方案方面取得重大進展,我就會給他們一個“Strong Hire”評級,然后讓他們問我問題。我從未見過哪位候選人到了這一步還能剩下多少時間。
這個問題還有一些待解決的地方,即單詞順序不重要、同義詞可以跨多個單詞。這些問題的解決方案都頗具挑戰(zhàn)性,也很有趣,我將在后續(xù)的文章中討論它們。
這個問題很有用,因為它允許候選人犯錯誤。軟件工程是一個永無止境的分析、執(zhí)行和改進的循環(huán)過程。這個問題為候選人提供了在每個階段展示自己能力的機會。如果想要獲得“Strong Hire”的面試評級,一個候選人需要具備如下的能力:
分析一個問題的陳述,找出模糊和不明確的地方,并在尋找解決方案和遇到新問題的過程中一直保持下去。為了提升效率,請盡可能早地完成這個階段,因為越到后面,從糾正錯誤的成本就越高。
用一種容易理解和解決的方式描述問題。對于這個面試題,最重要的一點是觀察到你可以在查詢中排列相應(yīng)的單詞。
實現(xiàn)你的解決方案。這涉及選擇最佳的數(shù)據(jù)結(jié)構(gòu)和算法,設(shè)計出可讀且在將來易于修改的邏輯。
回過頭來嘗試找出錯誤。這些可能是實際的錯誤,比如我忘記在上面插入“continue”語句,或者是性能問題,比如使用了不正確的數(shù)據(jù)結(jié)構(gòu)。
當問題的定義發(fā)生變化時,重復(fù)這個過程,并在適當?shù)臅r候調(diào)整你的解決方案。無論是在面試還是在現(xiàn)實生活中,知道什么時候做這兩件事都是一項關(guān)鍵的技能。
隨時掌握數(shù)據(jù)結(jié)構(gòu)和算法知識。并查集并不是一種常見的數(shù)據(jù)結(jié)構(gòu),但也不是那么罕見。確保自己了解各種工具的唯一方法是盡可能多地學習。
這些技能都無法從教科書上學到(除了數(shù)據(jù)結(jié)構(gòu)和算法之外)。獲得這些技能的唯一途徑是通過定期和廣泛的實踐,而這也正是公司所希望的:候選人不僅能夠掌握技能,而且能夠有效地應(yīng)用它們。考察這些候選人是面試的重點,而這個面試題在很長一段時間里幫了我大忙。
期 待
既然我寫了這篇文章,說明這個問題已經(jīng)被泄露了。從那時起,我一直在使用其他幾個問題,具體取決于我的心情(一直問一個問題很無聊)以及之前的面試官已經(jīng)問了哪些問題。其中一些面試題仍然在使用當中,所以我會保密。你可以期待在未來的文章中看到更多的面試題。
英文原文:
https://medium.com/@alexgolec/google-interview-problems-synonymous-queries-36425145387c
總結(jié)
以上是生活随笔為你收集整理的刷道谷歌泄漏的面试题:面试官想从中考察你什么?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一道泄露并遭禁用的谷歌面试题,背后玄机全
- 下一篇: 一次 Young GC 的优化实践