1. 程式人生 > >[演算法導論]網易MIT演算法導論課筆記(簡略版)

[演算法導論]網易MIT演算法導論課筆記(簡略版)

Introduction to Algorithm

  • 說明lgn是以2為底的對數
  • 編譯環境:g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
  • g++ -std=c++11 XX.cpp -o XX

第一課 演算法分析

第二課 漸近符號、遞迴及解法

  • f(n)的值總位於c1g(n)與c2g(n)之間或等於它們,那麼記f(n)=Θ(g(n))。
  • f(n)的值總小於或等於cg(n),那麼記f(n)=O(g(n))。
  • f(n)的值總大於或等於cg(n),那麼記f(n)=Ω(g(n))。
  • 用主方法求解遞迴式,
    如下圖:

另外這一篇BLOG詳細了介紹了這種方法的用途:BLOG

第三課 分冶法(Divide and Conquer)

  • 二分法 原始碼
  • 菲波那切數列 原始碼
  • 結:樸素遞迴演算法用時太多,實用價值不大,自底向上演算法效率為線性,較高,平時用較多,遞迴平方演算法效率為對數級,且程式設計可實現,實用價值很大。並且經過測試,當n值變很大後,遞迴平方演算法效率明顯高於自底向上演算法效率。BLOG

第四課 快排及隨機化演算法

  • 快速排序及隨機快速排序 原始碼
  • Hoare的程式碼對快排有重複的情況執行的更好。
  • 隨機化快速排序,其執行時間不依賴於輸入序列的順序Θ(nlgn),一般來說比歸併快3倍。
  • 這篇部落格對此進行了詳細的介紹我就摘錄了一些 BLOG
    自我小結:對隨機產生的陣列進行排序,1)可以發現插入排序沒有優勢、特別是陣列比較大時耗時太多;2)快速排序、隨機化快速排序、歸併排序效能不錯,然而兩種快排比歸併排序效能好點;3)當資料量變大時,可以看出效能排序為快速排序、隨機化快速排序、歸併排序、插入排序;4)由於這裡的陣列是由隨機數產生的,沒有顯示出隨機化快速排序的優勢,但是當陣列為已排序情況下隨機化快排將比快排效能好。

第五課 線性時間排序

  • 在最壞情況下,任何比較排序演算法都需要做Ω(nlgn)次比較。故堆排序和歸併排序都是漸進最優的比較排序演算法。
  • 計數排序 原始碼

它的優勢在於在對一定範圍內的整數排序時,它的複雜度為Ο(n+k)(其中k是整數的範圍),快於任何比較排序演算法。當然這是一種犧牲空間換取時間的做法,而且當O(k)>O(n*log(n))的時候其效率反而不如基於比較的排序(基於比較的排序的時間複雜度在理論上的下限是O(n*log(n)), 如歸併排序,堆排序)

原始碼中實現排序的程式碼是和計數排序一樣的。
基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些“桶”中,藉以達到排序的作用,基數排序法是屬於穩定性的排序,其時間複雜度為O (nlog(r)m),其中r為所採取的基數,而m為堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。

第六課 順序統計、中值

  • 隨機選擇演算法(一般選擇這個) 原始碼 :

執行時間的複雜度期望是Θ(n),最壞的情況複雜度為Θ(n2)。

相比於上面的隨機選擇,我們有另一種類似的演算法,它在最壞情況下也能達到O(n)。它也是基於陣列的劃分操作,而且利用特殊的手段保證每次劃分兩邊的子陣列都比較平衡;與上面演算法不同之處是:本演算法不是隨機選擇主元,而是採取一種特殊的方法選擇“中位數”,這樣能使子陣列比較平衡,避免了上述的最壞情況(Ө(n^2))。選出主元后,後面的處理和上述演算法一致。

參考部落格 BLOG

第七課 雜湊表(hash table)

  • 程式碼採用開放定址法處理衝突,包括線性探查、二次探查、雙重雜湊探查、隨機雜湊探查實現(探查法採用二次探查);雜湊函式採用簡單的除法雜湊函式;當插入一個新元素產生衝突次數過多時,進行再雜湊。原始碼

散列表是普通陣列概念的推廣,在散列表中,不是直接把關鍵字用作陣列下標,而是根據關鍵字通過雜湊函式計算出來的。

當實際儲存的關鍵字數目比全部的可能關鍵字總數要小時,採用散列表就成為直接陣列定址的一種有效的替代。

關鍵技術:
- 直接定址、雜湊定址
- 雜湊函式(除法雜湊、乘法雜湊、全域雜湊、完全雜湊)
- 碰撞處理方法:連結串列法、開放定址法(線性探查、二次探查、雙重雜湊、隨機雜湊)
- 再雜湊問題

第八課 全域雜湊和完全雜湊(Universal hashing and Perfect hashing)

  • 任何一個特定的雜湊函式都可能將特定的n個關鍵字全部雜湊到同一個槽中,使得平均的檢索時間為Θ(n)。為了避免這種情況,唯一有效的改進方法是隨機地選擇雜湊函式,使之獨立與要儲存的關鍵字。這種方法稱為全域雜湊(universal hashing)。全域雜湊在執行開始時,就從一組精心設計的函式中,隨機地選擇一個作為雜湊函式。因為隨機地選擇雜湊函式,演算法在每一次執行時都會有所不同,甚至相同的輸入都會如此。這樣就可以確保對於任何輸入,演算法都具有較好的平均情況效能.

  • 首先第一級使用全域雜湊把元素雜湊到各個槽中,這與其它的散列表沒什麼不一樣。但在處理碰撞時,並不像連結法(碰撞處理方法)一樣使用連結串列,而是對在同一個槽中的元素再進行一次雜湊操作。也就是說,每一個(有元素的)槽裡都維護著一張散列表,該表的大小為槽中元素數的平方,例如,有3個元素在同一個槽的話,該槽的二級散列表大小為9。不僅如此,每個槽都使用不同的雜湊函式,在全域雜湊函式簇h(k) = ((a*k+b) mod p) mod m中選擇不同的a值和b值,但所有槽共用一個p值如101。每個槽中的(二級)雜湊函式可以保證不發生碰撞情況。當第一級散列表的槽的數量和元素數量相同時(m=n),所有的二級散列表的大小的總量的期望值會小於2*n,即Ө(n)。

第九課 二叉搜尋樹(binary search trees)

  • 隨機化二叉搜尋樹本質上與隨機化的快速排序相等,既然二叉搜尋樹的BST排序和快速排序效率相同,那麼我們為什麼還要研究它呢?原因在於,本資料結構能夠支援更加快速的動態操作,諸如刪除、修改、插入等操作。

  • Randomized Binary Search Trees Code 原始碼

第十課 平衡搜尋樹

  • 紅黑樹的面試考點一般為,紅黑樹的特性和紅黑樹的基本操作:新增。

  • 紅黑樹(Red Black Tree) 是一種自平衡二叉查詢樹,是在電腦科學中用到的一種資料結構,典型的用途是實現關聯陣列。它的統計效能要好於平衡二叉樹(有些書籍根據作者姓名,Adelson-Velskii和Landis,將其稱為AVL-樹)

  • 紅黑樹的應用比較廣泛,主要是用它來儲存有序的資料,它的時間複雜度是O(lgn),效率非常之高。例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虛擬記憶體的管理,都是通過紅黑樹去實現的。

  • 紅黑樹的特性:
    (1) 每個節點或者是黑色,或者是紅色。
    (2) 根節點是黑色。
    (3) 每個葉子節點是黑色。 [注意:這裡葉子節點,是指為空的葉子節點!]
    (4) 如果一個節點是紅色的,則它的子節點必須是黑色的。
    (5) 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

  • 紅黑樹的基本操作:新增
    第一步:將紅黑樹當做一顆二叉查詢,將節點插入。
    第二步:將插入的節點著色為“紅色”。
    第三步:通過一系列的旋轉或著色等操作,使之重新成為一顆紅黑樹。

  • 其中第三步一共有根據父節點以及祖父節點、叔叔節點共分為5種情況。但核心思想都是將紅色的節點移到根節點;然後將根節點設為黑色。

  • 紅黑樹的基本操作:刪除
    第一步:將紅黑樹當作一顆二叉查詢樹,將節點刪除。
    第二步:通過“選轉和重新著色”等一系列來修正該樹,使之重新成為一顆紅黑樹。(分為四種情況)

第十一課 擴充的資料結構、動態有序統計和區間樹

方法論:如

第十二課 跳躍表

skip list 介紹

  • Skip list是一個用於有序元素序列快速搜尋的資料結構,由美國電腦科學家William Pugh發明於1989年。它的效率和紅黑樹以及 AVL 樹不相上下,但實現起來比較容易。Skip list是一個“概率型”的資料結構,可以在很多應用場景中替代平衡樹。Skip list演算法與平衡樹相比,有相似的漸進期望時間邊界,但是它更簡單,更快,使用更少的空間。
    Skip list是一個分層結構多級連結串列,最下層是原始的連結串列,每個層級都是下一個層級的“高速跑道”。

skip list 空間分析

第十三課 平攤分析,表的擴增,勢能方法

平攤分析概念

  • 給定一連串操作(這裡的操作具有一定的規律,例如有MULTIPOP操作的棧,或者vector),大部分的操作是非常廉價的,有極少的操作可能非常昂貴,因此一個標準的最壞分析可能過於消極了。因此,其基本理念在於,當昂貴的操作也別少的時候,他們的成本可能會均攤到所有的操作上。如果人工均攤的花銷仍然便宜的話,對於整個序列的操作我們將有一個更加嚴格的約束。本質上,均攤分析就是在最壞的場景下,對於一連串操作給出一個更加嚴格約束的一種策略。

  • 均攤分析與平均情況分析的區別在於,平均情況分析是平均所有的輸入,比如,INSERTION SORT演算法對於所有可能的輸入在平均情況下表現效能不錯就算它在某些輸入下表現效能是非常差的。而均攤分析是平均操作,比如,TABLEINSERTION演算法在所有的操作上平均表現效能很好儘管一些操作非常耗時。在均攤分析中,概率是沒有被包含進來的,並且保證在最壞情況下每一個操作的平均效能。因此n個元素插入的平攤代價(單次插入)為O(1)

平攤分析有三種方法

  • 聚集分析: 計算n次操作總共的時間,再做平均值。(一般不採用這個)
  • 記賬法: 對n個操作序列的不同操作賦予不同的平攤代價。
    一次操作的實際代價如果小於其平攤代價,則差額作為存款,存起來。
    一次操作的時間代價如果大於其平攤代價,則從存款中取出差額,以便完成此次操作。需要注意的是,存款不能為負的。
    因此n次操作過程中當前實際的總代價都小於等於當前的總平攤代價。這個平均代價需要自己去算。
  • 勢能法: 與整個操作序列相關的,定義Di為對資料結構Di-1做第i次操作後的結果,即記錄總的存款額。對比記賬法,記賬法為記錄每一次流水,勢能則只記錄總的存款額。
    勢能函式的構造可能不止一種,且一般需要很強技巧。
    例子的勢能函式的一種構造f(Di)=2 * i-2^([lgi])。

附錄

  • 表的擴增: 使用動態表,思想為vector擴充套件容量的方式,空間滿時重新分配兩倍當前大小的空間,並移動元素,釋放舊的空間。

  • 表的搜尋: 收縮則是當刪除一個後,元素個數小於表大小的1/4,而不是1/2時才收縮表。因為如果以1/2,則如果再立即插入,則會由擴增,從而增大開銷。

  • 平攤分析對於有實時需求的場合不太適合。(實時作業系統必須及時響應所要求的任務,在限定時間內完成任務。非實時的作業系統,多時間不是很敏感,對所要求的任務只是會保證完成,但在什麼時候完成,或用多長的時間完成就不一定了。)

第十四課 競爭性分析,自組織表

自組織表

  • 1.自組織表:
    定義兩種操作
    l n個元素的列表L,訪問(可能是查詢,也可以是其他操作)元素x的代價與元素在列表中的位置有關(從表頭到x的距離)。
    l 元素在L中的位置可以通過交換相鄰的元素來改變,而這個操作的代價為O(1)。
    如果考慮使用者的訪問可能是一系列的,而且一個元素被訪問後,再次被訪問的概率會增大,因此考慮對一個元素訪問後將該元素和其前驅的元素交換(代價為O(1)),從而減少其下次訪問的代價。
  • 1.1 一個操作序列,每次只發送一次操作請求。
    線上演算法(online):必須立即完成這步操作,而不管之後的操作是什麼(即不能預知後續操作)。
    離線演算法(offline):離線演算法可以假設可以預讀整個序列,從而可以對整個操作序列做優化。
    不管線上、離線演算法,其目標都是使得對整個操作序列的總的訪問代價最小。
  • 1.2 複雜度分析
    最壞情況分析。
    用線上演算法使用自組織表,對手每次都可能讓我們訪問最後一個元素,因此最壞時間為O(n*|S|),即表長乘操作序列的個數。
    平均情況分析。最壞情況無法避免,因此考慮平均情況。
    元素x被訪問的概率為P(x)(這相當於離線演算法)。則對於操作序列,期望的代價為:
    每個元素被訪問的概率與其位置乘積的和。
    因此最小期望為:把元素按訪問的概率從大到小排序。因此記錄元素被訪問的次數,並按訪問次數遞減的方式排序元素(訪問次數大於前驅的訪問次數時,進行交換)。因此對於元素x的操作,代價最多為2*rank(x),因為訪問需要rank(x),交換可能需要rank(x)。
    思想:前移思想。

應用:

這樣處理,對於搜尋的“流行詞”可能會有比較好的反應,因為在一個時期,流行詞被搜尋的次數會增加,而一旦過了流行期間,其位置可能就被新的流行詞替代了。這對於操作序列S的區域性反映非常好。
對於快取記憶體等其他情況下也可能用到。

競爭分析

  • 一個線上演算法A是a競爭的:如果存在一個常數k,滿足對於任何的操作序列S,滿足CA(S)<=a * Copt(S) + k
    即,演算法A對S的操作代價不大於其最優的離線演算法乘上 a,再加 k。

  • 對於自組織表MTF(Move to front,移前啟發式演算法:訪問一個元素後,就把元素移動到開頭)定理是4競爭的。(即便對手總是訪問最後一個元素。)對於兩個不同的代價,使用平攤分析的勢能法來確定這兩種代價的差距。

  • 4 * Copt(S)從而MTF為4競爭的。

對於競爭分析

  • 如果資料用連結串列表示,則從x位置移動到表頭的操作只需要常數,因此可以忽略其代價,這時可以證明相應的MTF則為2競爭的。

  • 如果表的開始的勢不為0,即L0和L0*不想等,比如有可能已經執行過一段時間了。這時候L0的最差情況為和L0*比是反序的,這樣逆序為n個元素的逆序,為O(n^2).
    這時候Cmtf(S)<=4* Copt(S)+O(n^2)。
    如果n的規模相對於S的次數變化不是太大,因此如果操作序列S中的操作為很大時,上式中的O(n^2)也是常量級別的,因此也是4競爭的。

  • 如果不是忽略置換的代價,而是一個常數級別的,如3,則相應的結果將改變競爭的常數,常數將不再是4倍。

第十五課 動態規劃,最長公共子序列

基本概念

  • 動態規劃(dynamic programming)與分治方法相似,都是通過組合子問題的解來求解原問題(在這裡,”programming”指的是一種表格法,並非編寫計算機程式)。

  • 分治方法將問題劃分為互不相交的子問題。動態規劃應用於子問題重疊的情況。動態規劃演算法對每個子問題只求解一次,將其解儲存在一個表格,從而無需每次求解一個子問題時都重新計算,避免了這種不必要的計算工作。

  • 動態規劃方法通常用來求解最優化問題(optimization problem)。這類問題可以有很多可行解,每個解都有一個值,我們希望尋找具有最優值的解。我們稱這樣的解為問題的一個最優解(an optimal solution),而不是最優解(the optimal solution),因為可能有多個解都達到最優值。

核心

我們通常按如下4個步驟來設計一個動態規劃演算法:
- 1.刻畫一個最優解的結構特徵。(optimal substruct)
- 2.遞迴地定義最優解的值。
- 3.計算最優解的值,通常採用自底向上的方法。
- 4.利用計算出的資訊構造一個最優解。

適合應用動態規劃方法求解的最優化問題應該具備的兩個要素:
- 最優子結構和子問題重疊。

問題

  • 鋼條切割(對遞迴演算法加入備忘機制): 原始碼
  • 矩陣鏈乘法(鋼條問題的升級版): 原始碼
  • 最長公共子序列: 原始碼
  • 最優二叉搜尋樹: 原始碼

第十六課 貪婪演算法,最小生成樹

貪心演算法的基本概念及應用

  • 貪心演算法:每一步都做出當時看起來最佳的選擇,即區域性最優解,寄希望這樣能導致全域性最優解。
  • 應用:活動選擇問題,哈夫曼編碼。

哈夫曼編碼

根據每個字元出現頻率,哈夫曼貪心演算法構造出字元的最優二進位制表示。
- 變長編碼:可以達到比定長編碼好的多的壓縮率,其思想是賦予高頻字元短碼字,低頻字元長碼字。
- 字首碼:沒有任何碼字是其他碼字的字首。

問題

已知:a:45,b:13,c:12,d:16,e:9,f:5,求變長編碼。
哈夫曼編碼原始碼

最小生成樹的基本概念及應用

  • 最小生成樹:有一個聯通的無向圖G=(V,E)(V是點的集合,E是點之間可能的連結)。我們希望找到一個無環的子集T屬於E,既能夠將所有的點連線起來,又具有最小的權重。因此,T必然是一顆樹。我們稱這樣的樹為生成樹,因為它是由圖G所生成的。我們稱求取該生成樹的問題為最小生成樹問題。
  • 應用:分散式系統,AT&T的記賬系統,電子電路設計。

相關知識

  • 圖的表示可以用一個二維矩陣,也可以用一個鄰接表。這樣就可以節省很多空間。
  • 優先佇列:在優先佇列中,元素被賦予優先順序。當訪問元素時,具有最高優先順序的元素最先刪除。優先佇列具有最高階先出 (first in, largest out)的行為特徵。
  • Kruskal演算法和Prim演算法。如果使用普通的二叉堆,那麼很容易地將這兩個演算法的時間複雜度限制在O(ElgV)的數量級內。但如果使用斐波那契數列;Prim演算法的執行時間將改善為O(E+VlgV)。此執行時間在|V|遠遠小於|E|的情況下較二叉堆有很大的改進。

問題

  • Kruskal演算法找到安全邊的辦法是,在所有連線森林中兩顆不同樹的邊裡面,找到權重最小的邊(u,v)。原始碼
  • Prim演算法所具有的一個性質是集合A中的邊總是構成一棵樹。原始碼

參考BLOG

第十七課 最短路徑演算法:Dijkstra演算法,廣度優先搜尋

廣度優先搜尋

  • 廣度優先搜尋是最簡答的圖搜尋演算法之一,也是許多重要的圖演算法的原型。Prim的最小生成樹演算法和Dijkstra的單源最短路徑演算法都使用了類似廣度優先的搜尋思想。
  • BFS原始碼

Dijkstra演算法

思想同Prim,Dijkstra演算法在執行過程中維持的關鍵資訊是一組節點集合S。

第十八課 最短路徑演算法:Bellman和差分約束系統

Bellman-Ford演算法

Bellman-Ford演算法解決的是一般情況下的單源最短路徑問題,在這裡,邊的權重可以為負值。

Bellman-Ford演算法可以大致分為三個部分
- 第一,初始化所有點。每一個點儲存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
- 第二,進行迴圈,迴圈下標為從1到n-1(n等於圖中點的個數)。在迴圈內部,遍歷所有的邊,進行鬆弛計算。
- 第三,遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況:d(v) > d (u) + w(u,v)則返回false,表示途中存在從源點可達的權為負的迴路。

差分約束系統

X1 - X2 <= 0
X1 - X5 <= -1
X2 - X5 <= 1
X3 - X1 <= 5
X4 - X1 <= 4
X4 - X3 <= -1
X5 - X3 <= -3
X5 - X4 <= -3

全都是兩個未知數的差小於等於某個常數(大於等於也可以,因為左右乘以-1就可以化成小於等於)。這樣的不等式組就稱作差分約束系統。
我們只要加一個源點X0,就可以把上面的問題轉化為單源最短路徑問題。

參考BLOG

第十九課 最短路徑演算法:點的最短路徑

回顧:單元最短路徑(V:point,E:edge)

  • 沒有加權的圖: 廣度優先演算法,時間複雜度O(V+E),即點的數量加上邊的數量。
  • 非負加權的圖: Dijkstra演算法,O(E+VlgV)。
  • 更一般的情況: Bellman-Ford演算法,O(VE)。

背景

在本節,我們考慮的問題是如何找到一個圖中所有節點之間的最短路徑。該問題在計算所有城市之間的交通道路距離時將出現。我們可以通過執行|V|次單源最短路徑演算法來解決所有節點對之間的最短路徑問題,每一次使用一個不同的節點作為源結點。

概述

  • 最短路徑和矩陣乘法:基於矩陣乘法的動態規劃演算法來解決所有節點對最短路徑問題。如果使用”重複平方”技術,演算法的執行時間為O(V3lgV)。
  • Floyd-Warshall演算法:動態規劃。O(V3)。
  • Johnson演算法:能在O(V2lgV+VE)的時間內解決所有結點對最短路徑問題,對於大型稀疏圖來說這是一個很好的演算法。

參考BLOG