一種通過程式設計面試的演算法 - malisper.me

banq發表於2022-01-04

在過去的幾年裡,我面試了十幾家公司,完成了大約 50 道個人演算法問題。我經常收到反饋說我在演算法問題上做得很好。在這篇文章中,我將分享我如何解決演算法問題。

 

思考過程

我使用的一個指導原則是,每個面試問題都是為了解決而設計的。你不會被要求在面試中證明費馬大定理。如果給你一些不可能的問題,那麼無論如何你都不會有太多的機會。

根據我的經驗,大約 80% 的演算法問題會歸結為幾個核心資料結構和演算法。我看到最多的資料結構是:

  • 雜湊表
  • 連結串列
  • 二叉搜尋樹

至於演算法:

  • 深度優先搜尋
  • 二分查詢
  • 排序演算法

(您可能不會期望實現二分搜尋或排序演算法,但您應該知道它們存在。)

您還應該瞭解另外兩種程式設計技術:

  • 動態規劃
  • 遞迴

假設您的問題的解決方案可以使用此列表中的專案解決,我們可以想出一個簡單的演算法來解決問題。

 

演算法

演算法是這樣的:

  1. 在給出演算法問題後,詢問您的解決方案需要的特定執行時間。幾乎可以肯定,面試官會告訴你。
  2. 過濾掉明顯與手頭問題無關的資料結構和演算法。這將消除大部分列表,通常會留下 2-3 個資料結構和演算法。
    1. 您可以過濾掉速度太慢的資料結構。如果你需要在 O(1) 時間內解決一個問題,那麼在解決方案中不可能使用二叉樹,因為二叉樹總是至少需要 O(log n) 時間。 
    2. 如果演算法無法用於給定的問題,您可以過濾掉它們。如果問題中沒有圖形,您就知道深度優先搜尋不相關。
  3. 瀏覽其餘資料結構的用例,看看哪些與手頭的問題相關。該問題的解決方案將是這些用例的組合。您需要做的就是將這些不同的用例拼湊在一起,然後您就會提出問題的解決方案。

 

案例 

讓我們來看看每個資料結構和演算法的執行時和主要用例。然後通過幾個示例,您可以瞭解使用該演算法是多麼容易。

資料結構:雜湊表O(1)

執行:插入、查詢和刪除。

用例:

  • 當您只需要儲存和查詢物件時。
  • 當您需要按某些屬性對不同組的物件列表進行分割槽時(基本上是 SQL 中的 group by 所做的)
  • 您需要計算列表中不同專案的數量。

  

資料結構:連結串列O(1)

執行: 在您已經有指標指向的節點的末尾或旁邊插入、查詢和刪除。

用例:

連結串列的主要用例圍繞著這樣一個事實,即連結串列維護元素的相對順序。在程式設計面試中,連結串列主要用作堆疊或佇列。

 

資料結構:二叉樹O(log n)

執行: 插入、查詢和刪除。

用例:當您需要按排序順序儲存資料時使用。這使您可以快速回答問題,例如有多少元素落入給定範圍或樹中第 N 個最高的元素是什麼。

 

資料結構:二分查詢

執行:O(log n)

用例:

  • 您需要在最接近另一個數字的排序陣列中找到該數字。
  • 您需要在排序陣列中找到比另一個數字大的最小數字。
  • 您需要在排序陣列中找到小於另一個數字的最大數字。
  • 如果由於某種原因無法使用雜湊表,則可以使用二分搜尋來檢查給定元素是否在排序陣列中。

 

資料結構:深度優先搜尋

執行:O(n)遍歷整個圖。

用例:

  • 在圖中查詢特定元素。
  • 找出圖的連通分量

 

資料結構:排序

執行:O(n log n)

用例:

  • 如果您需要按特定順序處理元素,則可以使用。首先按該順序排序,然後遍歷元素。
  • 可用於對稍後將執行二分搜尋的陣列進行排序。

動態規劃和遞迴有點不同,因為它們是解決演算法問題的通用技術,而不是特定演算法本身。這意味著它們沒有具體的執行時或用例。好訊息是經過一些練習,識別可以用動態程式設計或遞迴解決的程式變得相當容易。我的建議是練習一些需要動態程式設計或遞迴的問題,以便您對它們有所瞭解。全面解釋動態程式設計和遞迴超出了本文的範圍。

 

複雜案例

現在讓我們來看看幾個不同的面試問題,以及我們如何使用演算法來解決這些問題。

  • 問題 1:構建速率限制器

這是我在幾家不同公司多次看到的問題。

您需要編寫一個在任何 1 分鐘視窗內最多隻能呼叫 N 次的函式。例如,它在一分鐘內最多可以呼叫 10 次。如果函式被呼叫超過 N 次,它應該丟擲異常。該函式應該具有預期的 O(1) 效能。

看看我們可以使用的資料結構和演算法,看看哪些可以用來解決問題。然後嘗試看看如何使用這些資料結構來解決問題。在轉向解決方案之前先試一試。

  • 雜湊表
  • 連結串列
  • 二叉樹
  • 深度優先搜尋
  • 二分查詢
  • 排序
  • 動態規劃
  • 遞迴

解決方案

讓我們從消除所有顯然不能使用的資料結構和演算法開始:

  • 雜湊表
  • 連結串列
  • 二叉樹 - 太慢了。
  • 深度優先搜尋- 太慢了。也沒有執行 DFS 的圖表。
  • 二分查詢- 太慢了。也沒有排序陣列。
  • 排序- 太慢了。也沒有要排序的元素。
  • 動態規劃 – 無法將動態規劃應用於問題。
  • 遞迴 – 無法對問題應用遞迴。

只剩下雜湊表和連結串列。如果我們檢視雜湊表的常見用例,似乎沒有任何方法可以將它們應用於問題。我們不需要快速查詢不同的物件,也不需要將物件列表劃分為不同的組。這意味著我們可以從列表中劃掉雜湊表。

剩下的唯一資料結構是連結串列。檢視用例,只有兩個用於堆疊和佇列。我們可以使用其中任何一個來跟蹤函式在最後一分鐘被呼叫的次數嗎?是的!我們可以做的是保留一個佇列,該佇列在最後一分鐘內每次呼叫該函式時都有一個條目。每當呼叫該函式時,我們都會從佇列中刪除一分鐘前插入的所有條目。如果佇列的長度仍然大於 N,我們將丟擲異常。否則,我們將使用當前時間向佇列新增一個新條目。通過使用計數器跟蹤佇列的長度,我們可以確定 O(1) 的預期時間,該函式將具有 O(1) 的預期效能。

 

  • 問題#2:字謎

給定一個單詞列表,在輸入中生成一個單詞列表,這些單詞是列表中至少一個其他單詞的字謎。如果您可以重新排列一箇中的字母以獲得另一個,則兩個單詞是彼此的字謎。假設單詞具有恆定長度,執行時間應該是 O(n)。

在閱讀解決方案之前,再次嘗試自己解決問題。以下是完整的資料結構和演算法列表,供參考:

  • 雜湊表
  • 連結串列
  • 二叉樹
  • 深度優先搜尋
  • 二分查詢
  • 排序
  • 動態規劃
  • 遞迴

解決方案

讓我們首先從列表中刪除不可能用於此問題的專案:

  • 雜湊表
  • 連結串列
  • 二叉樹 - 太慢了。
  • 深度優先搜尋 – 沒有圖表。
  • 二分查詢 – 沒有排序陣列。
  • 排序 - 太慢了。
  • 動態規劃 – 無法將 DP 應用於問題。
  • 遞迴 – 無法對問題應用遞迴。

這讓我們只剩下雜湊表和連結串列。連結串列似乎與問題無關,因為堆疊或佇列看起來沒有任何幫助我們的方式。這樣就只剩下雜湊表了。

此處似乎相關的唯一雜湊表用例是能夠根據屬性將列表中的元素拆分為單獨的組。在這種情況下,如果有辦法根據哪些單詞是彼此的字謎將列表分成不同的組,那將給我們一個解決問題的方法:

[list=1]

  • 根據哪些單詞是彼此的字謎,將單詞列表分成不同的組。
  • 將包含多個單詞的所有組附加在一起。這將生成一個單詞列表,這些單詞是輸入列表中至少一個其他單詞的字謎。

    唯一需要解決的問題是找到一些我們可以用來將字謎組合在一起的屬性。我們需要找到某個函式 f 使得當 x 和 y 互為變位詞時 f(x) 與 f(y) 相同。我們可以使用兩個不同的函式來做到這一點:

    • 按字母順序對單詞的字元進行排序。因為字謎都是由相同的字母組成的。對於作為每個字謎的任何一對單詞,這將為我們提供相同的字串。
    • 製作每個單詞中每個字母出現的次數的字典。這個解決方案有點棘手,因為您需要以某種方式使用字典作為雜湊表中的鍵。有些語言有辦法做到這一點,而另一些則沒有。

    現在我們知道如何將互為字謎的單片語合在一起,我們可以將所有內容放在一起以生成解決方案!

     

    • 問題 #3:K 排序

    給定一個部分排序的物件陣列,對陣列進行排序。陣列中的每個元素與其實際位置的距離至多為 k。您沒有獲得此問題的目標執行時。

    像往常一樣,這裡是演算法和資料結構的列表: 

    • 雜湊表
    • 連結串列
    • 二叉樹
    • 深度優先搜尋
    • 二分查詢
    • 排序
    • 動態規劃
    • 遞迴

    解決方案

    讓我們首先看看我們是否可以推斷出有關執行時的任何資訊。我們可能實現的最快執行時間是 O(n),因為這是遍歷列表所需的時間。我們也可以始終對列表執行正常排序,這為我們提供 O(n log n) 的執行時間。讓我們看看是否有可能比 O(n log n) 做得更好。是否有可能像 O(n) 一樣快?好吧,如果 k=n,這個問題就變成了對列表進行排序的問題,所以不可能一直達到 O(n)。也許仍然有可能實現比 O(n log n) 更好的東西。現在讓我們看看哪些資料結構和演算法是相關的:

    • 雜湊表
    • 連結串列
    • 二叉樹
    • 深度優先搜尋 – 沒有圖表。
    • 二分查詢 – 沒有排序的陣列。
    • 排序 - 太慢了。
    • 動態規劃 - 不相關。
    • 遞迴 - 不相關。

    在剩下的資料結構中,唯一與問題相關的資料結構是二叉樹。二叉樹是列表中唯一處理元素排序的資料結構。如果您稍微思考一下如何使用二叉樹來解決問題,答案就很清楚了。我們可以保留最後 k 個元素的二叉樹。我們反覆從二叉樹中刪除最小的元素,並從輸入陣列中新增下一個元素。完整的演算法如下所示:

    • 將輸入陣列的前 k 個元素插入二叉樹。
    • 遍歷輸入陣列的其餘部分。在每次迭代中,從二叉樹中刪除最小的元素並將其新增到結果陣列中。然後將輸入列表中的當前元素新增到二叉樹中。
    • 一旦我們到達輸入陣列的末尾,重複從二叉樹中刪除最小的元素,直到樹為空。

    分析這個解決方案的執行時間,這給了我們 O(n log k) 的執行時間。有沒有可能做得比這更好?沒有更快的演算法似乎很直觀。什麼可能的演算法有 O(n) 和 O(n log k) 之間的執行時間,尤其是你在面試中不得不提出的演算法?這是一個非正式的證明,你不能比 O(n log k) 更快地解決問題。鑑於提出這一點並非易事,因此您不會在面試中提出來。如果你對證明不感興趣,你可以跳過它。

    假設您有一個比 O(n log k) 更快的演算法。我們可以使用該演算法提出比 O(n log n) 更快的排序演算法,這是不可能的。假設我們有 n/k 個單獨的列表,每個列表的大小為 k,每個列表的元素嚴格大於前一個列表的元素。如果將所有列表連線在一起,對它們執行 k-sorted,然後將每 k 個元素拆分為單獨的列表,則您將在不到 O(n log k) 的時間內對 n/k 個列表進行排序。這意味著平均而言,您在不到 O(n/(n/k) log k) = O(k log k) 的時間內對每個列表進行了排序,這是不可能的。因此,沒有任何 k 排序演算法比 O(n log k) 快。

    這意味著我們已經找到了問題的最佳解決方案。

    希望此時我已經讓您相信該演算法是解決演算法問題的有效方法。請注意,它不僅對解決面試中的問題有效,而且如果您在現實世界中遇到演算法問題,您可以使用它來檢查問題是否有由我們列表中的基本資料結構組成的解決方案。

    如果你想學習其他解決問題的方法,我強烈推薦《如何解決它》這本書。這本書涵蓋了大量不同的方法,您可以使用它們來解決任何問題。如何解決它對我今天處理任何型別的問題的方式產生了巨大的影響。

  • 相關文章