數據結構與算法分析
C 語言描述
引論
- 從N個數中確定第k個最大值,稱為選擇問題(selection problem).
- 不是所有的數學遞歸函數都能有效地(或正確地)由C的遞歸模擬來實現. 遞歸將反復進行直到基准情形出現.
- 遞歸的四條基本法則:
- 基准情形: 不需遞歸也能得到的解, 即終止條件.
- 不斷推進: 每次遞歸調用都要使解朝向基准情形推進.
- 設計法則: 假設所有的遞歸調用都能運行.
- 合法效益法則(compound interest rule): 在不同遞歸調用中不要做重復性的工作.
算法分析
- 算法(algorithm)是為求解一個問題需要遵循的, 被清楚地指定的單一指令的集合.
- 分治策略(divide and conquer):
- 分: 把問題分成兩個大致相等的子問題, 然后遞歸地求解.
- 治: 將兩個子問題的求解合並到一起並求解, 最后得到整個問題的解.
表, 棧和隊列
- 抽象數據類型(abstract data type, ADT)是一些操作的集合.
- 后綴表達式: 計算后綴表達式花費的時間是O(n), 每一步處理是棧操作的常數時間.
- 利用棧將一個標准形式的表達式轉換為后綴式:
- 當讀到一個操作數的時候,立即把它放到輸出中;
- 遇到操作符, 用棧緩存;
- 遇到左括號也入棧;
- 遇到右括號,就一直彈出至一個左括號;
- 遇到符號, 彈出至低優先級符號為止.
樹
- 二叉查找樹(binary search tree).
- 先序遍歷(preoder traversal): 根左右.
- 后序遍歷(postorder traversal): 左右根.
- 中序遍歷(inorder traversal): 左根右.
- 二叉樹:每個節點不能多於兩個兒子.
- 二叉樹的平均深度要比N小得多 --- 為(O(根號(N))).
- 二叉查找樹的平均深度為O(logN).
- 表達式樹: 表達式樹的葉子是操作數(operand), 其他節點是操作符(operator).
- 中序就是中綴表達式;
- 后序就是后序表達式;
- AVL樹是帶有平衡條件的二叉查找樹, 通過旋轉(rotation)來保證樹的平衡.
- 插入發生在外邊(左-左或右-右) --- 通過一次單旋來調整.
- 插入發生在內部(左-右或右-左) --- 通過雙旋來處理.
- 伸展樹(splay tree): 保證從空樹開始任意連續M次對樹的操作最多花費O(MlogN)時間.
- 一顆伸展樹每次操作的攤還代價是O(logN).
- 當一個節點被訪問后, 要經過一系列AVL樹的旋轉被放到根上. (適用於一個節點被訪問,不久后會再次被訪問).
B- 樹
- B- 樹是常用的查找樹, 但不是二叉樹.
- 階為M的B-樹具有下列特性:
- 樹的根或者是一片樹葉,或者兒子數在2和M之間.
- 除根外, 所有非樹葉節點的兒子數在[M/2]和M之間.
- 所有的葉子都在相同的深度上.
- 4階的B- 樹被稱為2-3-4樹, 3階的B- 樹被稱為2-3樹.
- B- 樹實際用於數據庫系統, 在哪里樹被存儲在物理的磁盤上而不是主存中.
散列
- 散列表的實現常常叫做散列(hashing).
- 散列是一種用於常數平均時間執行插入, 刪除和查找的技術.
- 散列最主要的是解決沖突(collision) --- 兩個關鍵字散列到同一個值的時候.
- 散列函數: 通常保證散列表的大小為一個素數.
- 取余法: Key mod TableSize;
- 盡量讓鍵值分布均勻.
- 解決散列沖突的方法:
- 分離鏈接法;
- 將散列到同一值的所有元素保留到一個表中.
- 開放定址法;
- 如果發生沖突, hash嘗試選擇另外的單元, 直到找出空的單元為止.
- 線性探測法;
- 平方探測法;
- 雙散列;
- 再hash; --- 表滿一半就再散列; 途中策略(middle-of-the-road) --- 當表到達某一個裝載因子的時候再進行再散列.
- 分離鏈接法;
- 處理數據大以至於裝不進主存的情況:
- 可擴散列(extendible hashing) --- 允許用兩次磁盤訪問執行一次Find操作.
- 用D代表根所使用的比特數, 稱其為目錄(directory), 目錄中的項數為2^D, dL為樹葉L所有元素共有的最高為的位數, dL <= D.
- 如果不夠存放插入的新關鍵字, 那么樹葉會被分成兩片新的樹葉, 目錄大小(比特的位數)將加1.
- 這種簡單的方法提供了大型數據庫Insert操作和Find操作的快速存取時間.
- 假設位模式(bit pattern)是均勻分布的
- 目錄的大小(2^D)為O(N^(1+1/M)/M), 如果M很小. 那么目錄可能過分大.
- 樹葉包含指向記錄的指針而不是實際的記錄, 這樣可以增加M的值.
優先隊列(堆)
- 優先隊列(priority queue) 也稱之為堆. --- 使用二叉查找樹實現優先級隊列.
- 二叉堆(binary heap)具有兩個性質:
- 結構性; --- 堆是一個完全二叉樹, 樹高O(logN). --- 完全二叉樹可以用一個數組表示而不需要指針.
- i位置的元素, 其左兒子在位置2i上, 右兒子在左兒子后的單元(2i+1)中, 父親在[i/2]上.
- 堆序性;
- 使操作快速執行的性質是堆序性(heap order), 最小堆需要最小元素在根節點上.
- 對堆的一次操作可能會破壞掉這兩個性質.
- 結構性; --- 堆是一個完全二叉樹, 樹高O(logN). --- 完全二叉樹可以用一個數組表示而不需要指針.
- 二叉堆插入節點時, 需要執行上慮策略(percolate up), 直到新元素找出正確的位置.
- 將新元素插入到滿二叉樹的末尾, 然后檢查是否父節點比其大, 如果大的話就與父節點交換位置.
- 刪除需要一種下濾的策略(percolate down):
- 刪除根節點后, 需要將滿二叉樹的最后一個元素正確的放到堆中, 先放到根節點, 再繼續和左右孩子進行比較, 選擇小的進行交換.
- 優先隊列的應用:
- 選擇問題;
- 事件模擬;
- d-堆是二叉堆的簡單推廣, 所有的節點都有d個兒子;
- 當優先隊列太大不能完全裝入主存的時候, d-堆是一個不錯的選擇.
- 左式堆:
- 左式堆(leftist heap)也具有結構特性和有序性, 左式堆也是一個二叉樹.
- 左式堆不是理想平衡的, 傾向於非常不平衡.
- 左式堆的左兒子的領路徑長至少與右兒子的領路徑長一樣大.
- 左式堆傾向於加深左路徑, 所以有路徑應該短.
- 左式堆最主要的應用是在堆的合並上.
- 斜堆:
- 斜堆(skew heap)是左式堆的自調節形式, 實現起來極其簡單.
- 斜堆與左式堆的關系類似於伸展樹與AVL樹之間的關系.
- 斜堆具有堆序的二叉樹, 但是不對樹的結構進行限制.
- 二項隊列結構:
- 一個二項隊列結構, 不是一棵堆序的樹, 而是堆序樹的集合, 稱為森林(forest).
- 二項樹的每一個節點將包含數據, 第一個兒子以及右兄弟, 二項樹中的各個兒子以遞減次序排序.
排序
- 插入排序(insertion sort);
- 希爾排序(shell sort)也稱為縮小增量排序(diminishing increment sort).
- 堆排序(heap sort)在實踐中慢於Sedgewick增量序列的希爾排序.
- 歸並排序(mergesort): 通過遞歸實現.典型的分治策略(divide-and-conquer).
- 每個遞歸調用, 局部都會聲明一個臨時數組.
- 快速排序(quicksort)是實踐中最快的已知排序算法, 快排也是一種分治的遞歸算法.
- 基本步驟:
- 如果中元素個數是0或1, 則返回.
- 取S中任一元素v, 稱之為軸點(pivot);
- 將S-v(S中剩余元素)分成兩個不相交的集合, S1和S2.
- 返回quicksort(S1), v, 和quicksort(S2).
- 隨機選取樞紐元, 三數中值分割法.
- 基本步驟:
- 小數組快排沒有插入排序快(N <= 20);
- 桶式排序(bucket sort).
- 外部排序(external sorting): 用來處理輸入很大的數據.
- 基本的外部排序是基於歸並排序中的Merge過程.
- 多路合並.
- 多相合並.
- 替換選擇.
不相交集ADT
- 等價關系:
- 自反性;
- 對稱性;
- 傳遞性.
- 保持不相交集合是非常簡答的數據結構.
- 路勁壓縮是自調整(self-adjustment)的最早形式之一, (伸展樹和斜堆).
圖論算法
- 深度優先搜索(depth-first search)是一個重要的技巧.
- 一個郵箱無圈圖有時被稱為DAG.
- 如果無向圖中從每個頂點到其他頂點都存在至少一條路徑, 則稱該無向圖是連通的(connected).
- 如果有向圖具有這樣的性質稱為強連通(strongly connected).
- 如果一個有向圖不是強連通, 但是是基礎圖(underlying graph) --- 去掉方向所形成的圖是連通的, 則稱該有向圖為弱連通(weakly connected).
- 完全圖(complete graph)是指每一對頂點之間都存在一條邊的圖.
- 圖可以通過鄰接矩陣和鄰接表進行表示.
拓撲排序
- 拓撲排序是對有向無圈圖的頂點的一種排序, 如果存在一條vi到vj的路徑, 那么在排序中vj出現在vi的后面.
- 拓撲排序並不是唯一的, 任何合理的排序都是可以的.
- 找出任意一個沒有入邊的頂點, 顯示出該頂點, 然后將它和它的邊一起從圖中刪除.
最短路徑算法
- 單源最短路勁問題: 找到給定頂點s到G中每個其他頂點的最短路徑.
- 廣度優先搜索(breadth-first search): 按層處理頂點, 距開始點最近的那些頂點首先被訪問, 最遠的點最后被訪問, 有點像樹的層次遍歷.
- dijkstra算法:
- 每個頂點都要保留一個臨時距離dv = 源點s到v的最短路徑長.
- 采用的是一種貪婪的策略(greedy algorithm). 貪婪算法一般分階段求解一個問題, 在每個階段它把當前出現的當作最優解去處理.
- 貪婪算法不是總能成功.
- 已知的鄰接點的臨時距離需要不斷調整直到最小的值.
- 可以寫一個遞歸程序跟蹤p數組留下的蹤跡.
- 對於稀疏圖, 使用優先級隊列對dijkstra最短路徑算法進行優化.
- 具有負邊值的圖, Dijstra算法是行不通的, 也不能向每條邊增加一個常量值delta進行去負邊.
- 無圈圖, 可以通過拓撲排序來改進Dijkstra最短路徑算法, 即選擇和更新在拓撲排序執行的時候進行.
- 無圈圖可以模擬某種滑雪問題;
- 無圈圖的一個更重要的用途是關鍵路徑分析法(critical path analysis).
- 所有點對最短路徑問題, 多源多匯最短路徑.
網絡流問題
- 解決最大流問題, 最簡單的思路是分階段解決, 構建一個殘余流的圖(residual graph), 對應殘余邊(residual edge).
- 從殘余圖Gr中尋找一條s到t的一條路徑, 稱這條路徑為增廣通路(augmenting path).
- 一旦注滿一條邊(使飽和), 則這條邊就要從殘余圖中去除.
- 最小費用流問題, 每條邊不僅有容量, 而且每個單位流還存在價格, 在最大流中找一個最小價格的流來.
最小生成樹
- 最小生成樹(minimum spanning tree), 無向圖中找最小生成樹比較容易, 在有向圖中比較困難.
- 無向圖中的最小生成樹就是由該圖的那些連接G的所有頂點的邊構成的圖, 其總價值最低.
- 包含途中所有頂點的最小的樹.
- Prim算法: 計算最小生成樹時, 使其連續地一步步生成, 在每一步都要把一個節點當作根往上加邊.
- 每一步添加一條邊和一個頂點到最小生成樹上.
- 與dijkstra最短路徑算法類似.
- Kruskal算法; 連續地按照最小的權值選擇邊(貪心的策略).
深度優先搜索
-
深度優先搜索(depth-first search)是對先序遍歷(preorder traversal)的推廣.
-
無向圖是連通的.
-
如果無向圖任一頂點刪除后, 圖仍是連通的, 那么這個無向連通圖稱為雙向連通的.
-
如果一個圖不是連通的, 刪除一點后圖不再連通, 那么這個頂點就叫做割點.
-
歐拉回路:
- 終點必須終止在起點上的歐拉回路只有當圖是連通的並且每個頂點的度(邊的條數)是偶數才又節能存在有效解.
- 所有頂點的度(邊的條數)均為偶數的任何連通圖必然有歐拉回路.
- 哈密爾頓圈問題(Hamiltonian cvcle problem) 是一個NP難問題.
- 哈密爾頓圈問題是要找一個圈, 該圈包含所有的頂點.
-
NP完全性:
- 存在大量重要的問題, 他們在復雜性上大體是等價的, 這一類問題被叫做NP完全(NP-complete)問題.
- 這些NP完全問題精確度的復雜性仍然需要確定並且在計算機理論科學方面仍然是最重要的開放性問題.
- 要么NP完全問題都有多項式時間解法, 要么都沒有多項式時間解法.
- NP(Nondeterministic polynomial-time)非確定型多項式時間.
- NP中的任意問題都能多項式歸約為NP完全問題.
- NP完全問題是NP問題中最難的問題.
-
NP完全問題的例子:
- 哈密爾頓圈問題是NP完全問題.
- 巡回售貨員是NP完全問題. 完全圖小於K值的簡單圈.
- 最長路徑問題.
- 裝箱問題(bin packing).
- 背包問題(knapsack).
- 圖的着色(graph coloring)問題.
- 團的問題(clique).
算法設計技巧
- 求解問題的五種通常類型的算法.
貪婪算法
- 貪心算法分階段工作, 在每一個階段, 認為鎖決定是好的, 不會考慮將來的后果.
- 一般得到的解都是局部最優解, 即次優解.
- 任務調度問題;
- Huffman編碼 --- 文件壓縮問題.
- 代表字母的二進制代碼用二叉樹來表示, 每個字符通過從根節點開始用0指示左分支用1指示, 右分支而以記錄路徑的方法表示出來. --- 這種樹叫做trie數.
- 總價值最小的滿二叉樹.
- 哈夫曼算法:
- 算法對一個由樹組成的森林進行;
- 一棵樹的權等於它的樹葉的頻率和.
- 任意選取最小權的兩顆樹T1和T2, 並任意形成新樹, 進行C-1次;
- 存在C棵單節點樹 --- 每個字符一顆樹;
- 算法結束時得到一顆樹, 這棵樹就是哈夫曼編碼樹.
- 最優前綴碼.
近似裝箱問題
- 聯機算法:
- 下項適合算法(next fit): 檢查剛剛裝進物品的箱子是否還能裝.
- 首次適合算法(first fit): 依次掃描箱子, 把新的物品放入足夠空間的箱子.
- 最佳適合算法(best fit): 把一個物品放到所有箱子中能夠容納它的最滿箱子中.
- 脫機算法:
- 首次適合遞減算法(first fit decreasing);
- 最佳適合遞減算法(best fit decreasing).
分治算法
- 分治算法(divide and conquer)由兩部分組成:
- 分(divide): 遞歸解決較小的問題.
- 治(conquer): 用子問題的解構造原問題的解.
動態規划
- 動態規划(dynamic programming)解決數學遞推公式的一種技巧, 當前狀態與之前的狀態相關.
- 最優二叉查找樹.
隨機化算法
- 一個隨機化算法的最壞運行時間幾乎總是和非隨機化算法的最壞運行時間相同.
x(i+1) = Ax(i) mod M
x(0)叫做隨機種子.- 跳躍表(skip list): 是在單鏈表上的擴展, 每個節點具有k個指針(每個節點的階是隨機確定的), 可以跳躍式地指向其他節點.
- 跳躍表類似於散列表, 它們都需要估計表中的元素個數(從而階的個數可以確定);
- 跳躍表如許多平衡查找樹實現方法一樣有效.
回溯算法
- 回溯算法(backtracking)相當於窮舉搜索的巧妙實現.
- 收費公路重建問題.
博弈
-
極小極大策略, 利用置換表(散列實現)節省大量的計算.
-
a-b裁剪(a-b pruning): 不需要進行求值的叫做a裁剪, 不會影響到min層的結果, 叫b裁剪.
-
攤還界比最壞情形界要弱, 但比等值的平均情形要強, 攤還要考慮整個操作序列而不是僅僅一次操作.
-
紅黑樹:
- 每一個節點或者着色為紅色, 或者着色為黑色.
- 根是黑色的.
- 如果一個節點是紅色的, 那其子節點都必須是黑色的.
- 從一個節點到一個NULL指針的每條路徑必須包含相同的黑色節點數.
-
1-2-3確定性跳躍表.
-
BB- 樹是帶有一個附加條件的紅黑樹: 一個節點最多可以有一個紅兒子.
-
AA結構要求從顏色轉換成層次.
- 水平連接是同一層次上的兒子之間的連接.
-
treap樹是一種二叉查找樹, 像跳躍表一樣使用隨機數並且對任意輸入都能給出O(logN)的期望時間性能.
-
k-d樹.
-
匹配堆. 最實用的斐波那契堆的變種, 具有兄弟指針, 前向指針(不代表父節點).