1. 程式人生 > 實用技巧 >Google的面試題長啥樣?看完被吊打

Google的面試題長啥樣?看完被吊打

本文翻譯自 Google 工程師/面試官 Alex Golec 的文章:Google Interview Questions Deconstructed: The Knight’s Dialer;來源:實驗樓,翻譯:實驗樓掃地阿姨,原文:https://medium.com/@alexgolec/google-interview-questions-deconstructed-the-knights-dialer-f780d516f029

作為一名Google的工程師和麵試官,今天是我第二次發文分享科技公司面試建議了。這裡先宣告:本文僅代表我個人的觀察、意見和建議。請勿當作來自Google或Alphabet的官方建議或宣告

下面這個問題,是我面試生涯中第一個問題;也是第一個被洩漏出來,以及第一個被禁掉的問題。我喜歡這個問題,因為它有以下優點:

  • 問題很容易表述清楚,也容易理解。

  • 這個問題有多個解。每個解都需要不同程度的演算法和資料結構知識。而且,還需要一點點遠見。

  • 每個解都可以簡單幾行程式碼實現,非常適合有時間限制的面試。

如果你是學生,或者求職者,我希望你通過本文能夠了解到,面試問題一般會是怎麼樣的。如果你也是面試官,我很樂意分享自己在面試中的風格和想法,如何更好地傳達資訊、徵求意見。

注意,我將使用Python寫程式碼;我喜歡Python因為它易學,簡潔,而且有海量的標準庫。我遇到的很多面試者也很喜歡,儘管我們推行“不限定語言”的政策,我面試90%的人都用Python。而且,我用的Python 3因為,拜託,這都2018年了。

問題

把你的手機撥號頁想象成一個棋盤。棋子走只能走“L”形狀,橫著兩步,豎著一步;或者豎著兩步,橫著一步。

現在,假設你撥號只能像棋子一樣走“L”形狀。每走完一個“L”形撥一次號,起始位置也算撥號一次。問題:從某點開始,在N步內,你可以撥到多少不同的數字?

討論

每次面試,我基本都會分成兩個部分:首先我們找出演算法方案,然後讓面試者在程式碼中實現。我說“我們找出演算法方案”,因為這個過程我不是沉默的獨裁者。在這樣高壓下,設計並實現一種演算法,45分鐘時間並不算充足。

我通常會讓面試者主導討論,讓他們去產生想法,我嘛,就在旁邊,時不時地洩漏一點點“天機”。面試者們能力越強,我需要洩漏的“天機”就越少;但是目前為止,我還沒遇到一點都不需要我提示的面試者。

有一點我想強調一下,重要的很:作為面試官,我的職責可不是坐那看著大家失敗搞砸。我想要給大家正面的反饋,給大家機會去展現大家最擅長的點。給他們提示,就像是在說:吶,這一步路我給你鋪上,但這只是為了讓你展示給我,你在後面的路上能走的更遠。

當聽完面試官的問題,你應該做什麼? 切記不要立刻就去寫程式碼,而是在黑板上試著一步一步去分解問題。分解問題能夠幫助你尋找到規律,特例等等,逐漸在大腦中形成解決方案。比如,你現在從數字6開始走,能走2步,會有如下組合:

6–1–8
6–1–6
6–7–2
6–7–6
6–0–4
6–0–6

一共有6種組合。你可以試著用鉛筆在紙上畫,相信我,有時候動手去解決問題會發生意想不到的事,比你盯著在腦袋裡想更神奇。

怎麼樣?你腦海裡有方案了嗎?

第0階:到達下一步

使用這個問題面試,最讓我驚訝的是,太多人都卡在了計算從某個特定點跳出時,一共有多少種可能,即鄰Neighbors。我的建議是:當你不確定時,先寫個佔位符,然後請求面試官能否晚點實現這一部分。

這個問題的複雜性並不在Neighbors的計算;我在意的是你如何計算出總數。所有花費在計算Neighbors上的時間其實都是浪費。

我會接受“讓我們假設有一個函式能給出我Neighbors”。當然,我也可能會讓你後面有時間再去實現這一步,你只需要這樣寫,然後繼續。

而且,如果一個問題的複雜性不在這裡,你也可以問我能不能先略過,一般我都是允許的。我倒是不介意麵試者不知道問題的複雜性在哪裡,尤其剛開始他們還沒有全面瞭解問題的時候。

至於Neighbors函式,因為數字永遠不變,你可以直接寫一個Map然後返回符合的值。

第1階:遞迴

聰明的你可能注意到了,這個問題可以通過枚舉出所有符合條件的數字,然後計算。這裡可以使用遞迴產生這些值:

這個方法可以,而且是在面試中最普遍的方法。但是請注意,我們產生了這麼多數字卻並沒有使用他們,我們計算完他們的個數後,就再也不去碰了。所以我建議大家遇到這種情況,儘量去想一下看有沒有更好的方案。

第2階:數不數數

怎麼在不產生這些數字的情況下計算出個數?可以做到,但需要一點點機智。注意從特定點跳出N次能夠撥到的數字個數,等於從它所有臨近的點跳出N-1次能夠撥到的數字個數的總和。我們可以表達為這樣的遞迴關係:

如果你這樣想,就會很直觀了,跳一次時:6有3個neighbors(1,7和0),當跳0次時每個數字本身算一次,因此每次你只能撥到3個數字。

怎麼會產生這樣機智的想法?其實,如果你學了遞迴,並且在黑板上好好研究,這一點就會變得顯而易見。這樣你就能繼續去解決這個問題,實際上就這一點就有多種實現方法,下面這個便是面試中最常見的:

就是這樣,結合這個函式計算出neighbors 就可以了。這時候,你就可以捏捏肩膀休息下了,因為到這裡,你已經刷掉很多人了。

接下來這個問題我經常問:這個方案的演算法理論速度如何?在這個實現中,每次呼叫count_sequences()都會遞迴地呼叫count_sequences()至少2次,因為每個數字至少有2個neighbors。這樣會導致runtime成指數增長。

對於跳1次到20次這樣的次數還可以,但是到更大的數字,我們就要碰壁。500次可能就需要整個宇宙的熱量來完成運算。

第3階:記憶

那麼,我們能做的更好麼?使用上面的方法,並不能。我喜歡這個問題,也是因為他能一層一層帶出大家的智慧,找到更高效的方法。為了找到更好的方法,讓我們看下這個函式是怎麼呼叫的,以count_sequences(6, 4)為例。注意這裡用C作為函式名簡化。

你可能注意到了,C(6, 2)運行了3次,每次都是同樣的運算並返回同樣的值。這裡最關鍵的點在於這些重複的運算,每次你使用過他們的值之後,就沒有必要再次計算。

怎麼解決這個問題?記憶。我們那些相同的函式呼叫和結果,而不是讓他們重複。這樣,在後面我們就可以直接給出之前的結果。實現方法如下:

第4階:動態設計

如果你再看看前面的遞迴關係,就會發現遞迴記憶的方案也有一點侷限性:

注意跳N次的結果僅僅取決於跳N-1次後呼叫的結果。同時,快取中包含著每個次數的所有結果。我之所以說這是個小侷限,因為確實不會造成真的問題,當跳的次數增長時,快取也只是線性增長。但是,畢竟,這還是不夠高效。

怎麼辦?讓我們再來看一看方案和程式碼。注意,程式碼中是從最大的次數開始,然後直接遞迴到最小的次數:

如果你把整個的函式呼叫圖想象成某種虛擬的樹,你就會發現我們在執行深度優先策略。這並沒有什麼問題,但是它沒有利用到淺依賴這個屬性。

如何實現廣度優先策略? 這裡就是一種實現方法:

這個版本比前面遞迴版好在哪裡?其實並沒有好很多,但是這個不是遞迴的,因此即使處理超大資料也很難崩潰。其次,它使用的是常量記憶體;最後,它仍舊是線性增長,即便處理200000次跳也只用不到20秒。

評估

到這裡,基本就算完了。設計並實現一個線性時的、產量記憶體的方案,在面試中是非常好的結果。在我的面試中,如果有面試者寫出動態程式設計設計,我通常會給他一個極高的評價:excellent!

當評估演算法和資料結構的時候,我經常會說:面試者對問題認識清晰,並且考慮到各方面的可能,當指出不足時他也能迅速改進並提高;最終,實現了一個不錯的解決方案。

當評估程式碼的時候,我最理想的說法是:面試者迅速並精確地把想法轉化為了程式碼;程式碼結構嚴謹,容易閱讀。所有特殊情況都有概括,並且認真檢查測試了程式碼,確保了沒有Bug。

總結

我知道,這個面試問題看上去似乎有點嚇人,尤其整個解釋下來非常繁瑣。但本文的目的和麵試中完全不一樣。最後,一點面試相關的技巧,以及一些好的習慣,分享給大家

  • 一定要手動來,從最小的問題開始解決。

  • 當你的程式在做無用的運算時,一定要注意去優化。減少不必要的運算能夠讓你的解決方案更加簡潔,說不定能因此發現更高效的方案。

  • 瞭解你的遞迴函式。在實際生產中,遞迴常常很容易出問題,但它仍舊是非常強大的演算法設計和策略。遞迴方案也總是有優化和提高的餘地。

  • 要常常去尋找記憶的機會。如果你的函式是目的性的,並且會多次呼叫相同的值,那麼就試著去儲存起來。