常見資料結構簡介
#Heap-like Data Structures
-
Heaps:小頂堆(二叉樹,完全樹),每個節點都比它的左右子樹小。按照層級從左到右插入節點,然後自下向上調整大小。刪除最小值的時候,直接刪除根節點(一直是最小的),然後把最後一個節點移到根節點,然後自頂向下調整大小。若給出一個已經建立好的完全樹,想調整為堆,則需要自底向上、從右到左地逐層調整,調整時還需要考慮子樹是否不再滿足堆條件,if so,自頂向下調整。由於堆是一個按照層次編號的完全樹,所以用一個數組作為資料結構。操作堆就是運算元組。
-
Binomial Queues:二項佇列,也叫Binomial heap,是一種形狀很有特點的樹。首先要知道什麼是二項樹:如圖為四個不同的二項樹,其規律為:度數為k的二項樹有一個根結點,根結點下有k個子女,每個子女分別是度數分別為k-1,k-2,…,2,1,0的二項樹的根。度數為k的二項樹共有2^{k}個結點,高度為k。在深度d處有
{\tbinom nd}
- Fibonacci Heaps:如圖,斐波那契堆由這樣一些小頂堆組成,其中每個節點ADT 如下:
1 |
struct FibonacciHeapNode { |
並且用min 指向最小的根節點。每個節點有一個指標指向其一個子女,它的所有子女由雙向迴圈連結串列連線,不同的小頂堆的根節點也是通過這種連結串列連線,叫做根表。
插入新元素時,只要將其作為單元素 F 堆,並跟原有堆合併,合併的方式是新 F 堆加入原 F 堆的根表中。
刪除元素時,先把 min 指向的最小節點刪除,然後將剩下的小頂堆分開,並按照二項堆組合的方式重新組合。
- Leftist Heaps:左偏堆,每個節點除了有左右孩子和鍵值外,還有一個距離。什麼是距離呢? 引用維基百科的話:「當且僅當節點 i 的左子樹且右子樹為空時,節點被稱作外節點(實際上儲存在二叉樹中的節點都是內節點,外節點是邏輯上存在而無需儲存。把一顆二叉樹補上全部的外節點,則稱為extended binary tree)。節點 i 的距離是節點 i 到它的後代中的最近的外節點所經過的邊數。特別的,如果節點 i 本身是外節點,則它的距離為 0;而空節點的距離規定為-1 。」如圖。除了堆的性質外,還有一條「節點的左子節點的距離不小於右子節點的距離。」其插入刪除等基本操作都是基於合併。怎樣合併呢?找到 root 鍵值最小的那個,用其右子樹與其他樹合併,若右子樹為空,把另外的樹直接弄過來,若此時右子樹距離比左子樹大了,那就交換左右子樹;若右子樹不空,把右子樹摘下來與其他樹合併,如此遞迴進行。刪除堆中最小值時,先刪除它,再把遺留的幾個子樹按照前述方法合併。左偏堆合併操作的平攤時間複雜度為O(log n)。
- Skew Heaps:斜堆,上述左偏堆就是一種特殊的斜堆。斜堆沒有距離的概念,其合併過程與左偏堆幾乎一樣,只是在每次合併之後都要左右子樹互換一下(啟發規則)。這樣往往能導致最後形成的樹中左子樹比右子樹深,所以是斜的。
#Graph Algorithms
- Breadth-First Search:廣度優先搜尋。圖可分為有向圖和無向圖(都可應用本演算法),其表示形式有圖形、鄰接表、臨界矩陣,注意圖中 Parent 和 Visited 兩個陣列。
-
Depth-First Search:深度優先搜尋,基本同上,除了搜尋的順序不同。搜尋過程中會涉及到回溯問題,而回溯可以用棧這種資料結構,也可以用遞迴的這種執行形式。廣度優先搜尋則不會有回溯的情況。
-
Connected Components:連通分量,對於無向圖,就是這樣的一個子圖:任意兩個節點都可以路徑可達,再加入該子圖之外的節點後就不滿足任意可達性了,所以也可以叫做最大連通子圖。其實每個獨立的無向圖都是連通分量。還有一個叫強聯通分量,是對應於有向圖的,這時就不太容易尋找一個有向圖的強連通分量了,因為要保證任意兩個節點是互相可達的。Kosaraju演算法、Tarjan演算法、Gabow演算法是目前比較有效的演算法。DSV 中用的哪種,我還沒看明白,等看完《演算法概論》中介紹的那種再說。
- Dijkstra’s Shortest Path:用於求解帶權有向圖(也可求無向圖)的單源最短路徑。如圖,我們用這樣一個表格來演示並記錄。Vertex 表示圖中的節點;Known 表示執行到目前為止,是否確定了該節點的最終最短路徑,初始為 F(否);Cost 表示目前為止從源節點到該節點的最短路徑,初始為 INF(無窮大); Path 是源點經過哪個節點到達的這個節點,初始為-1(無)。演算法執行的過程:假設源點為2,此時2為 T,尋找2的直接鄰節點,並更新 Cost(如果新 cost 比當前的小就更新,否則不更新),同時更新 Path 為2;然後,在所有F 的節點中,選擇當前 Cost 最大的一個,標記為 T,這個節點的全域性最短路徑就確定下來了,以此作為中間節點,尋找其直接鄰節點,並更新 Cost(注意,已經為 T 的不再更新)和 Path;如此迴圈,直到所有節點都為 T。
最終得到如下圖所示的表格。接下來就是把 Path 表示出來。比如執行到7的時候,7的 path 是5,5的是6,6的是2,所以7的最終 path 是 2 6 5 7.
-
Prim’s Minimum Cost Spanning Tree:Prim 演算法,對於給定的無向圖,找出最小生成樹。最小生成樹就是包含圖中所有節點和部分邊是一個樹,並且其權值之和最小。該演算法的執行過程就是從源節點出發,選擇權值最小的鄰近節點,再以此為當前節點,選擇未訪問的權值最小的鄰近節點,如此迴圈,若到頭了,則回溯。總體來說是大 DFS 中包含著小 BFS。具體的執行過程可以使用Dijkstra演算法中使用的表格形式。
-
Topological Sort (Using Indegree array):對於一個DAG,一定存在拓撲排序,使得對於 uv,在拓撲排序中,u 一定在 v 前面。這裡使用直觀的Kahn演算法:有兩個集合,L 表示已經排好的,S 表示入度為0的節點;每次從 S 中取出一個節點 n 放入 L 中,並檢視從 n 出發的節點中是否有入度減為0的,若有,加入到 S 中,然後再從 S 中取節點,迴圈。。。如果最後剩下邊,說明該圖是有環的,不存在拓撲序列,否則最後得到的 L 即拓撲序列。從演算法的執行過程來看,它是基於佇列的。
-
Topological Sort (Using DFS):基於 DFS 的拓撲排序,這裡 S 是所有出度為0的節點的集合。對於每個節點,遞迴訪問以它為尾的所有節點,並標記為 Visited,並按照遞迴的退出順序把節點加入到 L 中。這種方法其實也很直觀,出度為0的節點,回溯到頭一般就是入度為0的節點了。深度優先遍歷會用到遞迴或棧。
-
Floyd-Warshall (all pairs shortest paths):留著,太複雜了。
-
Kruskal Minimum Cost Spanning Tree Algorithm:Kruskal 演算法,對於給定的無向圖,找出最小生成樹。其方法很簡單,就是從所有邊中,依次挑選最小的邊形成樹的一個邊,加入到當前的樹中,如果這個邊使樹成為圖,就捨棄,尋找次最小的,直到所有的節點都進入這個樹中。
#Dynamic Programming
Calculating nth Fibonacci number,
Making Change,
Longest Common Subsequence:動態規劃適用於有最優子結構和重疊子問題的問題。最優子結構是指區域性最優解能決定全域性最優解,這樣我們才能把問題分解成子問題來解決。子問題重疊性質是指「在用遞迴演算法自頂向下對問題進行求解時,每次產生的子問題並不總是新問題,有些子問題會被重複計算多次。動態規劃演算法正是利用了這種子問題的重疊性質,對每一個子問題只計算一次,然後將其計算結果儲存在一個表格中,當再次需要計算已經計算過的子問題時,只是在表格中簡單地檢視一下結果,從而獲得較高的效率。
」最常用的例子是斐波那契數列。其求解最常用的演算法是如下遞迴:
1 |
function fib(n) |
雖然通過遞迴把問題分解為子問題了,但是,遞迴過程中很多子問題被重疊計算了,比如當n=5時,fib(5)的計算過程如下:
1 |
fib(5) |
改進的方法是,我們可以通過儲存已經算出的子問題的解來避免重複計算:
1 |
array map [0...n] = { 0 => 0, 1 => 1 } |
其他
- Disjoint Sets:並查集,也叫union-find algorithm,因為該資料結構上的主要操作是 find 和 union,還有一個基本的 makeset 操作。
資料結構是一個樹,每個節點除了有值外,還有一個指標指向父節點。一個樹就是一個集合,樹的根節點代表這個集合。由於只需要一個指標指向父節點,一般用一個數組表示這棵樹。
資料結構說清楚了,接下來談談其操作。makeset 的作用是建立一個只含X 的集合。find 的作用是找到 X 的根節點,即找到 X 屬於哪個集合。union 的作用是把 x 和 y 代表的集合合併。這三個操作的程式碼非常簡單,但是得到的樹會很高很偏,在之後的操作中效率就低了。對此有兩種優化方法:路徑壓縮和按秩合併。下面給出最終程式碼,在註釋處註明其改進:
1 |
|