程式設計師必備:技術面試準備手冊

胡西瓜發表於2015-08-31

這份清單,既是一份有助於對這些題目做深入研究的快速指南和參考,也算是電腦科學課程中不能忘記的基礎知識總結,因此並不可能全面覆蓋所有內容。它也可以作為 gist Github 上公開,人人都可以編輯和補充。

一、資料結構基礎

陣列

定義

  • 按順序連續儲存資料元素,通常索引從0開始
  • 以集合論中的元組為基礎
  • 陣列是最古老,最常用的資料結構

知識要點

  • 索引最優;不利於查詢、插入和刪除(除非在陣列最末進行)
  • 最基礎的是線性陣列或一維陣列

    陣列長度固定,意味著宣告陣列時應指明長度

  • 動態陣列與一維陣列類似,但為額外新增的元素預留了空間

    如果動態陣列已滿,則把每一元素複製到更大的陣列中

  • 類似網格或巢狀陣列,二維陣列有 x y 索引

時間複雜度

  • O(1)索引:一維陣列:O(1),動態陣列:O(1)
  • O(n)查詢:一維陣列:O(n),動態陣列:O(n)
  • O(log n)最優查詢:一維陣列:O(log n),動態陣列:O(log n)
  • O(n)插入:一維陣列:n/a,動態陣列:O(n)

連結串列

定義

  • 結點儲存資料,並指向下一結點

    最基礎的結點包含一個資料和一個指標(指向另一結點)

    • 連結串列靠結點中指向下一結點的指標連線成鏈

要點

  • 為優化插入和刪除而設計,但不利於索引和查詢
  • 雙向連結串列包含指向前一結點的指標
  • 迴圈連結串列是一種終端結點指標域指向頭結點的簡單連結串列
  • 堆疊通常由連結串列實現,不過也可以利用陣列實現

    堆疊是“後進先出”(LIFO)的資料結構

    • 由連結串列實現時,只有頭結點處可以進行插入或刪除操作
  • 同樣地,佇列也可以通過連結串列或陣列實現

    佇列是“先進先出”(FIFO)的資料結構

    • 由雙向連結串列實現時,只能在頭部刪除,在末端插入

時間複雜度

  • O(n)索引:連結串列:O(n)
  • O(n)查詢:連結串列:O(n)
  • Linked Lists: O(n)最優查詢:連結串列:O(n)
  • O(1)插入:連結串列:O(1)

雜湊表或雜湊圖

定義

  • 通過鍵值對進行儲存
  • 雜湊函式接受一個關鍵字,並返回該關鍵字唯一對應的輸出值

    這一過程稱為雜湊(hashing),是輸入與輸出一一對應的概念

    • 雜湊函式為該資料返回在記憶體中唯一的儲存地址
要點
  • 為查詢、插入和刪除而設計
  • 雜湊衝突是指雜湊函式對兩個不同的資料項產生了相同的輸出值

    所有的雜湊函式都存在這個問題

    • 用一個非常大的雜湊表,可以有效緩解這一問題
    • 雜湊表對於關聯陣列和資料庫檢索十分重要

時間複雜度

  • O(1)索引:雜湊表:O(1)
  • O(1)查詢:雜湊表:O(1)
  • O(1)插入:雜湊表:O(1)

二叉樹

定義

  • 一種樹形的資料結構,每一結點最多有兩個子樹
    • 子結點又分為左子結點和右子結點

要點

  • 為優化查詢和排序而設計
  • 退化樹是一種不平衡的樹,如果完全只有一邊,其本質就是一個連結串列
  • 相比於其他資料結構,二叉樹較為容易實現
  • 可用於實現二叉查詢樹
    • 二叉樹利用可比較的鍵值來確定子結點的方向
    • 左子樹有比雙親結點更小的鍵值
    • 右子樹有比雙親結點更大的鍵值
    • 重複的結點可省略
    • 由於上述原因,二叉查詢樹通常被用作一種資料結構,而不是二叉樹

時間複雜度

  • 索引:二叉查詢樹:O(log n)
  • 查詢:二叉查詢樹:O(log n)
  • 插入:二叉查詢樹:O(log n)

二、搜尋基礎

廣度優先搜尋

定義

  • 一種在樹(或圖)中進行搜尋的演算法,從根結點開始,優先按照樹的層次進行搜尋
    • 搜尋同一層中的各結點,通常從左往右進行
    • 進行搜尋時,同時追蹤當前層中結點的子結點
    • 當前一層搜尋完畢後,轉入遍歷下一層中最左邊的結點
    • 最下層最右端是最末結點(即該結點深度最大,且在當前層次的最右端)

要點

  • 當樹的寬度大於深度時,該搜尋演算法較優
  • 進行樹的遍歷時,使用佇列儲存樹的資訊
    • 原因是:使用佇列比深度優先搜尋更為記憶體密集
    • 由於需要儲存指標,佇列需要佔用更多記憶體

時間複雜度

  • O(|E| + |V|)查詢:廣度優先搜尋:O(|E| + |V|)
  • E 是邊的數目
  • V 是頂點的數目

深度優先搜尋

定義

  • 一種在樹(或圖)中進行搜尋的演算法,從根結點開始,優先按照樹的深度進行搜尋
    • 從左邊開始一直往下遍歷樹的結點,直到不能繼續這一操作
    • 一旦到達某一分支的最末端,將返回上一結點並遍歷該分支的右子結點,如果可以將從左往右遍歷子結點
    • 當前這一分支搜尋完畢後,轉入根節點的右子結點,然後不斷遍歷左子節點,直到到達最底端
    • 最右的結點是最末結點(即所有祖先中最右的結點)

要點

  • 當樹的深度大於寬度時,該搜尋演算法較優
  • 利用堆疊將結點壓棧
    • 因為堆疊是“後進先出”的資料結構,所以無需跟蹤結點的指標。與廣度優先搜尋相比,它對記憶體的要求不高。
    • 一旦不能向左繼續遍歷,則對棧進行操作

時間複雜度

  • O(|E| + |V|)查詢:深度優先搜尋:O(|E| + |V|)
  • E 是邊的數目
  • V 是結點的數目

廣度優先搜尋 VS. 深度優先搜尋

  • 這一問題最簡單的回答就是,選取何種演算法取決於樹的大小和形態
    • 就寬度而言,較淺的樹適用廣度優先搜尋
    • 就深度而言,較窄的樹適用深度優先搜尋

細微的區別

  • 由於廣度優先搜尋(BFS)使用佇列來儲存結點的資訊和它的子結點,所以需要用到的記憶體可能超過當前計算機可提供的記憶體(不過其實你不必擔心這一點)
  • 如果要在某一深度很大的樹中使用深度優先搜尋(DFS),其實在搜尋中大可不必走完全部深度。可在 xkcd 上檢視更多相關資訊。
  • 廣度優先搜尋趨於一種迴圈演算法。
  • 深度優先搜尋趨於一種遞迴演算法

三、高效排序基礎

歸併排序

定義

  • 一種基於比較的排序演算法
    • 將整個資料集劃分成至多有兩個數的分組
    • 依次比較每個數字,將最小的數移動到每對數的左邊
    • 一旦所有的數對都完成排序,則開始比較最左兩個數對中的最左元素,形成一個含有四個數的有序集合,其中最小數在最左邊,最大數在最右邊
    • 重複上述過程,直到歸併成只有一個資料集

要點

  • 這是最基礎的排序演算法之一
  • 必須理解:首先將所有資料劃分成儘可能小的集合,再作比較

時間複雜度

  • O(n)最好的情況:歸併排序:O(n)
  • 平均情況:歸併排序:O(n log n)
  • 最壞的情況:歸併排序:O(n log n)

快速排序

定義

  • 一種基於比較的排序演算法
    • 通過選取平均數將整個資料集劃分成兩部分,並把所有小於平均數的元素移動到平均數左邊
    • 在左半部分重複上述操作,直到左邊部分的排序完成後,對右邊部分執行相同的操作
  • 計算機體系結構支援快速排序過程

要點

  • 儘管快速排序與許多其他排序演算法有相同的時間複雜度(有時會更差),但通常比其他排序演算法執行得更快,例如歸併排序。
  • 必須理解:不斷通過平均數將資料集對半劃分,直到所有的資料都完成排序

時間複雜度

  • O(n)最好的情況:歸併排序:O(n)
  • O(n log n)平均情況:歸併排序:O(n log n)
  • 最壞的情況:歸併排序:O(n2)

氣泡排序

定義

  • 一種基於比較的排序演算法
    • 從左往右重複對數字進行兩兩比較,把較小的數移到左邊
    • 重複上述步驟,直到不再把元素左移

要點

  • 儘管這一演算法很容易實現,卻是這三種排序方法中效率最低的
  • 必須理解:每次向右移動一位,比較兩個元素,並把較小的數左移

時間複雜度

  • O(n)最好的情況:歸併排序:O(n)
  • O(n2)平均情況:歸併排序: O(n2)
  • O(n2)最壞的情況:歸併排序: O(n2)

歸併排序 VS. 快速排序

  • 在實踐中,快速排序執行速率更快
  • 歸併排序首先將集合劃分成最小的分組,在對分組進行排序的同時,遞增地對分組進行合併
  • 快速排序不斷地通過平均數劃分集合,直到集合遞迴地有序

伯樂線上推薦閱讀:

四、演算法型別基礎

遞迴演算法

定義

  • 在定義過程中呼叫其本身的演算法
    • 遞迴事件:用於觸發遞迴的條件語句
    • 基本事件:用於結束遞迴的條件語句

要點

  • 堆疊級過深和棧溢位
    • 如果在遞迴演算法中見到上述兩種情況中的任一個,那就糟糕了
    • 那就意味著因為演算法錯誤,或者問題規模太過龐大導致問題解決前 RAM 已耗盡,從而基本事件從未被觸發
    • 必須理解:不論基本事件是否被觸發,它在遞迴中都不可或缺
    • 通常用於深度優先搜尋

迭代演算法

定義

  • 一種被重複呼叫有限次數的演算法,每次呼叫都是一次迭代
    • 通常用於資料集中遞增移動

要點

  • 通常迭代的形式為迴圈、forwhileuntil語句

  • 把迭代看作是在集合中依次遍歷每個元素
  • 通常用於陣列的遍歷

遞迴 VS. 迭代

  • 由於遞迴和迭代可以相互實現,兩者之間的區別很難清晰地界定。但必須知道:
    • 通常遞迴的表意性更強,更易於實現
    • 迭代佔用的記憶體更少
  • (i.e. Haskell)函式式語言趨向於使用遞迴(如 Haskell 語言)
  • 命令式語言趨向於使用迭代(如 Ruby 語言)
  • 點選 Stack Overflow post 瞭解更多詳情

遍歷陣列的虛擬碼(這就是為什麼使用迭代的原因)

貪婪演算法

定義

  • 一種演算法,在執行的同時只選擇滿足某一條件的資訊
  • 通常包含5個部分,摘自維基百科:
    • 候選集,從該集合中可得出解決方案
    • 選擇函式,該函式選取要加入解決方案中的最優候選項
    • 可行性函式,該函式用於決策某一候選項是否有助於解決方案
    • 目標函式,該函式為解決方案或部分解賦值
    • 解決方案函式,該函式將指明完整的解決方案

要點

  • 用於找到預定問題的最優解
  • 通常用於只有少部分元素能滿足預期結果的資料集合
  • 通常貪婪演算法可幫助一個演算法降低時間 複雜度

虛擬碼:用貪婪演算法找到陣列中任意兩個數字間的最大差值

這一演算法無需比較所有數字兩兩之間的差值,省略了一次完整迭代。

相關文章