基礎資料結構演算法筆記


簡單的就直接空白了…

資料結構 Data Structure

List

Array

Stack: LIFO

Queue: FIFO

Hash Table

Heap

  • Priority queue
  • 保證父節點比子節點小,不保證左右的大小關係
  • Balance. 所以當取出 root 或新增 node 時,都是以最右下角的位置為對象。新增 node 時是新增在最右下角的空位,再一路往上與父節點比較看是非需要調換;取出 root 時,把最右下角的 node 移到 root,比較兩個子節點,與 3 者中最小者調換

Binary Search Tree

  • 保證每一個 subtree 的右側一定大於左側
  • 非 balance
  • 新增一定都成為葉節點
  • 刪除時若該節點有子節點,則把從左子樹最大值取上來,左子樹最大值就是其最右下的葉節點
  • 因為非 balance,所以蒐尋時間不一定。若平衡,為 O(log n);若極度不平衡成一條線,那就是 O(n)

Self-balancing Binary Search Tree

AVL Tree

  • 不平衡時會做 rotation,維持在平衡的狀態,可避免 BST 退化為鏈狀。rotation 前後 in-order traversal 順序相同。
  • 每一個節點需要維持其高度資訊:先為樹補上所有 null 葉節點,null 節點高度為 -1,其他節點是其子節點較大者 +1
  • 每一個節點需要計算其 Balance Factor (BF) :左子節點的高度減右以節點的高度。所以若 BF 為負,代表右子樹比較大棵。需要注意若只差 1 還是容許的平衡狀態。
  • Rebalance
    • 當新增一個節點,整條路上的節點的高度資訊要更新。然後,從新節點往根部方向走兩層,也就是走到新結點的爺爺節點,看爺爺的 BF 是否小於 -1 或大於 1,如果是,那就代表要 Rebalance。怎則做 Rebalance 取決於剛剛怎麼從新節點走到爺爺的路,如果一路走左分枝上來,就是 LL,一路走右分枝就是 RR,同理還有 LR 和 RL,四種形態有對應的旋轉方式。
    • 當刪除一個節點,不管是把其左子樹最大值補位上來,還是把右子樹最小值補位上來,都是要整棵樹檢查。整棵樹檢查方式是任意挑一個頁節點往上檢查即可,如果一路到根部,都沒有出現 BF 小於 -1 或大於 1,那就平衡。若一旦發現就要根據其路徑立即做 LL/RR/LR/RL 對應的旋轉方式。旋轉完成後再從頭檢查一次,直到某次一路檢查到根部,都沒有出現 BF 小於 -1 或大於 1。

Red Black Tree

  • 也是自平衡的 BST。AVL Tree,為了平衡會要經過好多次旋轉,而 RB Tree 的結構設計,使得任何不平衡都會在三次旋轉之內解決。
  • 然而嚴格上來說紅黑數只是幾乎平衡的 BST,因為他只保證「從 root 出發抵達葉節點的最長徑不會超過最短的兩倍」
  • 藉由以下 5 項特徵
  1. 紅黑樹中的每一個節點不是黑色就是紅色
  2. 根節點一定是黑色
  3. 原本葉節點的 NULL 指標都另外新增補個葉節點給他,然後最終每一個葉節點一定是黑色
  4. 如果某個節點是紅色,那麼其兩個子節點必定是黑色,不能有兩個紅色節點相連
  5. 站在任何一個節點上,所有從該節點走到任意葉節點路經的黑色節點數必定相同
  • 根據上述特徵的第 4 點與第 5 點,紅黑樹中路徑可能的長度最小值一定是全部節點皆為黑色,而路徑可能的長度最大值一定是紅色黑色相間,如此便確保最長路徑不會超過最短路徑的兩倍的特性。
  • 新稱或刪除資料,都會根據節點位置有不同得對應方式,包含些修正一些節點的顏色,以及旋轉,但旋轉都是在三次以內,即可將其修正成符合紅黑數特徵。

排序 Sort

Bubble sort

  • 每次看相鄰 2 個數,確保左邊小於右邊,從最右側開始每次向左滑 1 單位,如此當回滑到最左側時,左手的值就是整串數列的最小值,這樣是一輪
  • 時間複雜度是 O(n*n),因為預期比較動作就是進行 n!,但可以做的優化是如果有某一輪完全沒做 swap,那就可以終止。所以會得到 best case 是 O(n),就是在原本就已經排好的狀況

Selection sort

  • 一次把數張撲克牌攤開在手上,最直覺的排法就是,眼光從作掃到右,挑出其中最小的,移到最左邊
  • 時間複雜度是 O(n*n)

Insertion sort

  • 一張一張把撲克牌拿起來,插到對的位置。如果用 array 來看,就是以最左邊為起點,往右一個一個加進來到對的位置
  • 時間複雜度是 O(n*n),因為預期比較動作就是進行 n!。best case 是 O(n),就是在原本就已經排好的狀況,每當多看一個數,都發現其大於左側已確定的數列中的最大值,也就是每一輪的第一次比較就發現本輪結束

Heap sort

  • 創造 heap 需要 O(n log n),因為有 n 個數,而插入每個數的預期時間與高度相關
  • 從 heap 取出每次都取 root,但 heap 要因此調整,時間是 O(log n),所以全部取完是 O(n log n)

Merge sort

  • 把數列兩兩一組,完成排序後,再把兩組合併。合併方式為比較兩組當中最左側的值,將較小者取出。這樣為完成一輪
  • 時間是 O(n log n),因爲每輪各組總共 n 次比較,而總共有 log n 輪

Quick sort

  • 隨機選取 pivot,把比 pivot 小的放一邊大的放一邊。再對左右兩邊做相同處理
  • 如果運氣好 pivot 選得好,造成每次左右兩邊數量一樣,那時間就是 O(n log n);如果運氣不好,造成每次的都只有的單邊,那時間就是 O(n*n)

從左到右,所以不管原本的數列是已排列或未排列

僅用於已排列,每次與中間位置比較,所以搜尋時間是 O(log n)

[Graph] BFS 廣度優先 Breadth First Traversal

  • 原本 queue 裡只有 root,把他取出,如果不符合欲搜尋對象,就把其子節點他們全部推進 queue 裡,這樣為一輪。之後再從 queue 取出一個進行下一輪
  • 圖中有迴圈的情形也適用
  • 1 2 3 4 5

範例

[Graph] DFS 深度優先

  • 換成搭配 stack
  • Inorder traversal:造訪順序為 左 -> 中 -> 右 (4 2 5 1 3)
  • Preorder traversal:造訪順序為 中-> 左 -> 右 (1 2 4 5 3)
  • Postorder traversal:造訪順序為 左 -> 右 -> 中 (4 5 2 3 1)

[Graph] Bellman-Ford 找出最短路徑

  1. 把起點設為 0,其他點暫設為無限大
  2. 工作對象是「所有的邊」,每一輪都是把所有的邊都看一次,看邊上的兩點,從 A 到 B 或從 B 到 A 的值,是否可以更小
  3. 只要每一輪都還有更新任一點的值,都代表還要進行下一輪,直到某一輪完全沒有更新任何點上的值時結束,此時終點上的值就是最短路徑
  • 若有 v 個點,e 個邊,時間為 O(ev)。原因是每一輪都要檢查 e個邊,而最多能跑幾輪?
    在第 1 輪結束後,得到的是起點最多經過一條邊到達其他點的最短距離;第 2 輪結束後,得到的是起點最多經過兩條邊到達其他點的最短距離;那要到底幾輪才會是到終點的最短距離呢?
    圖中任兩點的最短距離,頂多經過 v-1 個邊,也就是從起點途經所有點才抵達終點的狀況。因為如果某一點造訪兩次,那代表有迴圈出現,這基本上純粹成本增加
    如此說來,第 v-1 輪結束後,得到的是起點最多經過 v-1 條邊到達其他點的最短距離,有就是我們最多需要執行的輪數,所以總時間為 O(ev)
  • 負數邊和圖中有迴圈皆可用,但若同時有負數邊和迴圈,則會出現迴圈一直跑就成本一直遞減的狀況,所以最短路徑其實不存在,也就是說當執行輪數到達 v 卻還有值能更新時,即可判斷為最短路徑不存在

[Graph] Dijkstra’s 找出最短路徑

  1. 把起點設為 0,其他點暫設為無限大
  2. 工作對象從起點開始,更新起點的相鄰點上的值,如此為一輪
  3. 從目前看過的點中,選擇值最小者,作為第二輪的開頭
  4. 如此重複,直到選擇到終點為工作點為止
  • 若有 v 個點 e 個邊,時間為 O(v\*v)。原因是每一輪結束,都會確認下由起點到達某一點的最短路徑。總共可有 v-1 個點要確認,所以要進行 v-1 輪。而每一輪,要把當前工作點所一步能及的所有相鄰點做更新,每一點的相鄰點可為 v-1,所以總共是 O(v*v)
  • Bellman-Ford 比較:v 個點最多能有 v*(v-1)/2 個邊,且 v 大多數時候小於 e,所以 vv 大部份時候小於 ve,所以 Dijkstra’s 比較快,尤其是稀疏圖的 v 就是偏小的時候。而且若 Dijkstra’s 的實作使用 Fibonacci heap 存圖,則時間複雜度可提升到 O(e + v log v)
  • Greedy 在每一輪得到從起點到某一點的最短路徑,所以這也造成無法處理有負數邊的狀況。因為每一輪都只能看到目前所看過的點和邊,如果是還沒看到的邊而且為負數邊,那就會造成以為已經得到到從起點到某一點的最短路徑但其實不然,因為未來某負數邊的加入會改變這一結論。(無法處理負迴圈,但可以處理迴圈)

[Graph] A* 找出最短路徑

A*Dijkstra’s 的發展,Dijkstra’s 每一輪會決定出從起點到某一點的最短路徑,但該點以結論來說,有可能根本不在最短路徑上,所以最終來看就是一種浪費

A* 導入了試探權重 heuristic cost,僅是一種人為的、根據其他已知客觀條件所給的推測、建議性質參數,在選擇下個點時提供幫助。在每一輪,每一點的試探權重與當前由起點抵達該點的成本相加,取其中最小者作為下一點,如果試探權重很符合真實狀況,那就會更快探索到終點並結束,但這並不見得是最佳路徑,因為根本有些點還沒被探索到,你無法確定是不是如果經過那些點會得到一條更佳的路徑,所以目前的到的結果只能說是考慮進試探權重的最佳路徑


Author: Chris Chung
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Chris Chung !
  TOC