這份清單,既是一份有助於對這些題目做深入研究的快速指南和參考,也算是電腦科學課程中不能忘記的基礎知識總結,因此並不可能全面覆蓋所有內容。它也可以作為 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. 快速排序
- 在實踐中,快速排序執行速率更快
- 歸併排序首先將集合劃分成最小的分組,在對分組進行排序的同時,遞增地對分組進行合併
- 快速排序不斷地通過平均數劃分集合,直到集合遞迴地有序
伯樂線上推薦閱讀:
- 《視覺直觀感受 7 種常用的排序演算法》
- 《匈牙利 Sapientia 大學的 6 種排序演算法舞蹈視訊》
- 《視訊:6 分鐘演示 15 種排序演算法》
- 《SORTING:視覺化展示排序演算法的原理,支援單步檢視》
- 《VisuAlgo:通過動畫學習演算法和資料結構》
四、演算法型別基礎
遞迴演算法
定義
- 在定義過程中呼叫其本身的演算法
- 遞迴事件:用於觸發遞迴的條件語句
- 基本事件:用於結束遞迴的條件語句
要點
- 堆疊級過深和棧溢位
- 如果在遞迴演算法中見到上述兩種情況中的任一個,那就糟糕了
- 那就意味著因為演算法錯誤,或者問題規模太過龐大導致問題解決前 RAM 已耗盡,從而基本事件從未被觸發
- 必須理解:不論基本事件是否被觸發,它在遞迴中都不可或缺
- 通常用於深度優先搜尋
迭代演算法
定義
- 一種被重複呼叫有限次數的演算法,每次呼叫都是一次迭代
- 通常用於資料集中遞增移動
要點
-
通常迭代的形式為迴圈、for、while和until語句
- 把迭代看作是在集合中依次遍歷每個元素
- 通常用於陣列的遍歷
遞迴 VS. 迭代
- 由於遞迴和迭代可以相互實現,兩者之間的區別很難清晰地界定。但必須知道:
- 通常遞迴的表意性更強,更易於實現
- 迭代佔用的記憶體更少
- (i.e. Haskell)函式式語言趨向於使用遞迴(如 Haskell 語言)
- 命令式語言趨向於使用迭代(如 Ruby 語言)
- 點選 Stack Overflow post 瞭解更多詳情
遍歷陣列的虛擬碼(這就是為什麼使用迭代的原因)
1 2 3 4 5 6 7 8 |
Recursion | Iteration ----------------------------------|---------------------------------- recursive method (array, n) | iterative method (array) if array[n] is not nil | for n from 0 to size of array print array[n] | print(array[n]) recursive method(array, n+1) | else | exit loop |
貪婪演算法
定義
- 一種演算法,在執行的同時只選擇滿足某一條件的資訊
- 通常包含5個部分,摘自維基百科:
- 候選集,從該集合中可得出解決方案
- 選擇函式,該函式選取要加入解決方案中的最優候選項
- 可行性函式,該函式用於決策某一候選項是否有助於解決方案
- 目標函式,該函式為解決方案或部分解賦值
- 解決方案函式,該函式將指明完整的解決方案
要點
- 用於找到預定問題的最優解
- 通常用於只有少部分元素能滿足預期結果的資料集合
- 通常貪婪演算法可幫助一個演算法降低時間 複雜度
虛擬碼:用貪婪演算法找到陣列中任意兩個數字間的最大差值
1 2 3 4 5 6 |
greedy algorithm (array) var largest difference = 0 var new difference = find next difference (array[n], array[n+1]) largest difference = new difference if new difference is > largest difference repeat above two steps until all differences have been found return largest difference |
這一演算法無需比較所有數字兩兩之間的差值,省略了一次完整迭代。