1. 程式人生 > >讀書筆記_演算法第四版(一)

讀書筆記_演算法第四版(一)

演算法第四版(謝路雲譯)

第1章 基礎

1.1 基礎程式設計模型

l  Java程式的基本結構;原始資料型別與表示式;語句;簡便記法;陣列;靜態方法;API;字串;輸入輸出;二分查詢。

以下為“答疑”和“練習”中的知識點:

l  Math.abs(-2147483648)返回-2147483648(整數溢位)。

1.2 資料抽象

l  使用抽象資料型別;抽象資料型別舉例;抽象資料型別的實現;更多抽象資料型別的實現;資料型別的設計。

以下為“答疑”和“練習”中的知識點:

1.3 揹包、佇列和棧

l  許多基礎資料型別都和物件的集合有關。具體來說,資料型別的值就是一組物件的集合,所有操作都是關於新增、刪除或是訪問集合的物件。揹包、佇列和棧就是,三者不同之處在於刪除或是訪問物件的順序不同。

l  用到泛型和迭代:(Iterable介面要實現publicIterator<Item> iterator()方法)

public class Bag<Item> implementsIterable<Item>

public class Queue<Item> implementsIterable<Item>

public class Stack<Item> implementsIterable<Item>

原始資料型別則使用Java的自動裝箱將boolean、byte、char、short、int、float、double、long轉換為Boolean、Byte、Character、Short、Integer、Double、Long。並且用自動拆箱轉換回來。

l  揹包:是一種不支援從中刪除元素的集合資料型別,它的目的就是幫助用力收集元素並迭代遍歷所有收集到的元素,迭代的書序不確定且與用例無關。

l  先進先出佇列(FIFO):一種基於先進先出策略的集合型別,用集合儲存元素的同時儲存它們的相對順序,並且入列順序和出列順序相同。

l  下壓棧(LIFO):一種基於後進先出策略的集合型別,元素的處理順序和它們被壓入的順序正好相反。

l  順序棧實現:先實現定容棧FixedCapacityStackOfStrings類,只能儲存String型別,並且大小固定;然後用泛型實現FixedCapacityStack類;再新增resize(int capacity)方法並更改push和pop使之可以調整自動大小保證不會溢位且使用率大於四分之一,即ResizableStack類;最後實現Iterable介面,完成ResizingArrayStack類。

l  物件遊離:Java垃圾收集策略是回收所有無法被訪問的物件的記憶體。儲存著一個不需要的物件的引用就稱為遊離。要避免物件遊離只要將引用設為null即可。

l  連結串列棧實現:結點類Node,表頭插入結點,表頭刪除結點,表尾插入結點,連結串列的遍歷。然後實現Stack類

l  連結串列佇列的實現,揹包就是去掉pop()的Stack。

l  自己嘗試實現順序佇列:固定大小的迴圈佇列FixedCapacityQueue類,可調整大小可迭代遍歷的迴圈佇列ResizingArrayQueue類。

l  其他資料結構(除去揹包、佇列、棧):父連結樹、二分查詢樹、字串、二叉堆、散列表(拉鍊法)、散列表(線性探測法)、圖的鄰接連結串列、單詞查詢樹、三向單詞查詢樹。

以下為“答疑”和“練習”中的知識點:

l  為什麼Java不允許泛型陣列:專家們仍在爭論這一點,初學者請先了解共變陣列(covariantarray)和型別擦除(type erasure)。

l  Stack的內部類Node經過javac編譯後會生成一個Stack$Node.class檔案。

l  Java陣列支援foreach遍歷(for(inti : int[] array))。

l  應當避免使用寬介面(實現了很多功能以提升適用性),因為這樣無法保證高效,並且可能出現意外情況,其實是累贅。

l  可以用Stack類來將十進位制轉化為二進位制(習題1.3.5),結合Stack和Queue來反轉佇列(習題1.3.6),中序表示式轉化為後續表示式(習題1.3.10),有專門的連結串列練習最好自己多動手寫程式碼,雙向佇列(雙向連結串列實現和動態陣列實現),

1.4 演算法分析

l  科學方法:1、細緻地觀察真實世界的特點,通常還要有精確地測量;2、根據觀察結果提出結社模型;3、根據模型預測未來的時間;4、繼續觀察並核實預測的準確性;5、如此反覆知道確認預測和觀察一致。

l  一個程式執行的總時間主要和兩點有關:執行每條語句的耗時(取決於計算機、Java編譯器和作業系統),執行每條語句的頻率(取決於程式本身和輸入)。二者相乘並將程式中所有指令的成本相加得到執行總時間。

l  時間複雜度(程式執行時間的增長數量級)和空間複雜度。

l  理解問題規模的上界和時間複雜度的下界。

l  注意事項:大常數,非決定性的內迴圈,指令時間,系統因素,不分伯仲,對輸入的強烈依賴,多個問題參量。

l  Java物件要有24位元組的物件頭,而陣列如int陣列就是24+4N位元組(N為奇數時還有填充位元組),double陣列則是24+8N位元組。String物件總共會使用40個位元組(16位元組表示物件,3個int例項變數個需要4位元組,加上陣列引用的8位元組和4個填充位元組)+(24+2N)位元組字元陣列(字串間共享)。

以下為“答疑”和“練習”中的知識點:

l  雙調查詢,僅用加減實現的二分查詢(斐波那契查詢),扔雞蛋,扔兩個雞蛋,兩個棧可以實現佇列(三種方法,要注意細節),一個佇列實現棧,兩個佇列實現棧,熱還是冷。

1.5 案例研究:union-find演算法

動態連通性,union-find演算法,三種實現。

1、 用集合來儲存,標識為某一個觸點,即quick-find演算法。

2、 用森林來儲存,每個觸點的id[i]指向自己或連通分量中另一個觸點,指向自己的則是根觸點,為quick-union演算法,但是每次兩樹合併是不確定的,不保證比quick-find演算法快。

3、 用森林儲存,改進quick-union,為加權quick-union演算法,儲存樹的節點數,每次把小樹連線到大樹上,保證對數級別。

4、 使用路徑壓縮的加權quick-union,是當前最優演算法,但並非所有的操作都能在常數時間內完成。

第2章 排序

2.1 初級排序演算法

l  排序演算法可以分為兩類:除了桉樹呼叫需要的棧和固定數目的例項變數之外無需額外記憶體的原地排序演算法,以及需要額外記憶體空間來儲存另一份陣列副本的其他排序演算法。

l  選擇排序:選擇陣列最小放到陣列前面,每次選擇最小放到陣列第k小的位置。N2/2次比較和N次交換,O(n2),執行時間和輸入無關,資料移動是最少的。

l  插入排序:陣列左端有序,每次插入一個元素是比較,較小則前移。平均情況需要~N2/4次比較和~N2/4次交換,最壞情況需要~N2/2次比較和~N2/2次交換,最好情況需要N-1次比較和0次交換。插入排序對部分有序陣列很有效,事實上,當倒置的數量很少時,插入排序很可能比本章中任何演算法都要快。

l  希爾排序:基於插入排序,間隔h進行間隔元素插入排序,h逐漸減小為1。高效的原因是權衡了子陣列的規模和有序性。可以用於大型陣列。執行時間達不到平方級別。屬於O(n1.5)級別。有經驗的程式設計師有時會選擇希爾排序,因為對於中等大小的陣列它的執行時間是可以接受的,它的程式碼量很小,且不需要使用額外的記憶體空間,在下面的幾節中我們會看到更加高效的演算法,但是除了對於很大的N,它們可能只會比希爾排序快2倍(可能還達不到),而且更復雜。

l  通過提升速度來解決其他方式無法解決的問題是研究演算法的設計和效能的主要原因之一。

以下為“答疑”和“練習”中的知識點:

l  所有主鍵相同插入排序比選擇排序更快。逆序陣列選擇排序比插入排序更快。元素只有三種值的隨機陣列插入排序仍然是平方級別的。出列排序,按照限定條件每次選出最小(大)值然後將有序的放入底部。

2.2 歸併排序

l  歸併排序:將陣列分成兩半分別排序,然後將結果歸併起來。是分治思想的體現。

l  自頂向下的歸併:從大分到小,使用遞迴,但實際上最終還是從兩個元素開始歸併。需要0.5NlgN至NlgN次比較,最多需要訪問陣列6NlgN次。

l  自底向上的歸併:從兩個到多,使用迭代。需要0.5NlgN至NlgN次比較,最多需要訪問陣列6NlgN次。

l  歸併排序告訴我們,當能夠用其中一種方法解決一個問題時,你都應該試試另一種。

l  沒有任何基於比較的演算法能夠保證使用少於lg(N!)~NlgN次比較將長度為N的陣列排序。(借用二叉比較樹可以證明)

以下為“答疑”和“練習”中的知識點:

l  所有元素相同時歸併排序執行時間是線性的。如果是多個值重複則歸併排序仍然是線性對數的。

歸併排序的三項改進:用插入排序加快小陣列的排序速度,用array[mid]array[mid+1]檢測陣列是否已經有序,通過在敵對中交換引數來避免陣列複製。

連結串列排序(選擇排序,插入排序,氣泡排序,所有排序)。

歸併有序的佇列,然後自底向上實現有序佇列的歸併排序。自然的歸併排序(自適應歸併排序),利用陣列中已有的有序部分。

打亂連結串列(直接的就是每次隨機選出一個,但執行時間是平方級別的。或者利用陣列來實現打亂,O(n)時間複雜度和O(n)空間複雜度。題目要求線性對數級別時間和對數級別空間。最開始的思路我的思路是遞迴實現的,每次隨機排序左右兩個連結串列,但我只是簡單的將兩個連結串列的前後順序打亂,這樣的結果是鄰近的元素仍然是在附近的,並沒有實現,沒有想出來突破點。只要靠搜尋引擎了。百度“打亂連結串列”居然沒有結果,只有百度“shuffling a linked list”才有結果而且都是英文的。用谷歌搜尋“shufflinga linked list”發現了一個人在Quora上發的討論,然後看了他在StackOverflow上的回答,但是程式碼是python的,不過差不多能看懂,然後才發現,合併兩個連結串列時,應該是每次隨機從一個連結串列中選出頭元素來合併成新連結串列,這樣才是真正的打亂。然後寫出程式碼就行了,這裡迭代的寫法可能麻煩點,因為要靠大小來確定左右連結串列的大小或者左右連結串列的尾結點。)。

間接排序,不改變陣列的歸併排序,返回int[] perm,perm[i]為第i小的元素位置,其實就是對perm進行歸併,只不過比較時要用array和perm來取元素比較。

三向歸併排序,把陣列分成三部分而不是兩部分進行歸併排序,其實就是多一些判斷,本質上還是差不多的,執行時間仍然是線性對數級別的。

2.3 快速排序

l  可能是應用最廣泛的排序演算法了,原因是實現簡單,適用於各種不同的輸入資料且在一般應用中比其他排序演算法都要快得多,而且是原地排序,時間複雜度是O(nlgn)。

l  快速排序是一種分支的排序演算法,它將一個數組分成兩個子陣列,將兩部分獨立地排序,切分(partition)的位置取決於陣列的內容。

l  快速排序平均需要~2NlnN次比較(以及1/6的交換),最多需要約N2/2次比較,但隨即打亂陣列能夠預防這種情況。

l  快速排序的改進:小陣列插入排序更快,所以小陣列用插入排序。三取樣切分,取三個元素比較用中間大小的元素作為切分元素。熵最優排序,重複值較多時用三向切分,即分為大於、等於、小於。對於只有若干不同主鍵的隨機陣列,三向切分快速排序是線性的。對於大小為N的陣列,三向切分快速排序需要~(2ln2)NH次比較,其中H為由主鍵值出現頻率定義的夏農資訊量。

以下為“答疑”和“練習”中的知識點:

l  將陣列平分希望提高效能,用現有演算法,是做不到的。

將已知只有兩種主鍵值得陣列排序。

非遞迴的快速排序,藉助棧,存取每個要進行切分的子陣列首尾位置。

快速三向切分,用將重複元素放置於子陣列兩端的方式實現一個資訊量最優的排序演算法。

2.4 優先佇列

l  優先佇列是一種抽象資料型別,它表示了一組值和對這些值的操作,優先佇列最重要的操作就是刪除最大元素和插入元素。

l  優先佇列的一些重要的一個應用場景包括模擬系統、任務排程、數值計算等。

l  優先佇列初級實現:無序陣列實現(惰性方法),有序陣列實現(積極方法),連結串列實現(無序或者有序)。這些要不就是插入操作為O(n)要不就是刪除為O(n)。而使用堆的佇列,可以插入刪除都為O(logn)。

l  堆(二叉堆):實際上就是一棵二叉樹,一般用陣列實現。第0個元素不使用,k的子結點是2k和2k+1。而k的父結點是k/2向下取整。插入元素:將新元素加到陣列末尾,增加堆的大小並讓這個新元素上浮到合適的位置。刪除最大元素:從陣列頂端刪去最大元素並將陣列的最後一個元素放到頂端,減小堆的大小並讓這個元素下沉到合適的位置。

l  對於一個含有N個元素的基於堆的優先佇列,插入元素操作只需不超過lgN+1次比較,刪除最大元素操作需要不超過2lgN次比較。

l  索引優先佇列:僅對索引進行優先佇列排序。

l  堆排序:將所有元素插入一個查詢最小元素的優先佇列,然後再重複呼叫刪除最小元素的操作來將他們按順序刪去。用下沉操作由N個元素構造堆只需少於2N次比較以及少於N次交換。

l  先下沉後上浮(sink、swim):大多數在下沉排序期間重新插入堆的元素會被直接加入到堆底,Floyd在1964年觀察發現,我們正好可以通過免去檢查元素是否到達正確位置來節省時間,在下沉中總是直接提升較大的子結點直至到達堆底,然後再使元素上浮到正確的位置。這個想法幾乎可以將比較次數減少一半,但是這種方法需要額外的空間,因此在實際應用中只有當比較操作代價較高時才有用。(例如在進行字串或者其他鍵值較長型別的元素排序時)

l  堆排序在排序複雜性研究中有這著重要的地位,因為它是我們所知的唯一能夠同時最優地利用空間和時間的方法,在最壞的情況下也能保證~2NlgN次比較和恆定的額外空間。空間緊張時如嵌入式系統或低成本的移動裝置中很流行。但現代系統的許多應用中很少使用它,因為它無法利用快取,很少和相鄰元素進行比較,因此快取未命中的次數要遠遠高於大多數比較都在相鄰元素之間進行的演算法,如快速排序、歸併排序,甚至是希爾排序。另一方面用堆實現的優先佇列在現代應用程式中越來越重要,因為它能在插入操作和刪除最大元素操作混合的動態場景中保證對數級別的執行時間。

以下為“答疑”和“練習”中的知識點:

l  MaxPQ使用泛型的Item是因為這樣delMax()的用力就不需要將返回值轉換為某種具體的型別,比如String。一般來說,應該儘量避免在用例中進行型別轉換。

堆中不利用第0個元素是因為能夠稍稍簡化計算,另外將第0個元素的值用作哨兵在某些堆的應用中很有用。

可以用優先佇列實現棧、佇列、隨機佇列等資料結構。

2.5 應用

l  將各種資料排序(實現Comparable介面的資料型別皆可以):交易事務,指標排序,不可變的鍵(如String、Integer、Double、File),廉價的交換(引用),多種排序方法,多鍵陣列(定義比較器Comparator),使用比較器實現優先佇列,穩定性(能否保留重複元素的相對位置)。

l  用Comparator介面來代替Comparable介面能夠更好地將資料型別定義和兩個該烈性的物件應該如何比較的定義區分開來。

l  我們應該使用哪種排序演算法:取決於應用場景和具體實現。快速排序是最快的通用排序演算法。(選擇排序,插入排排序,希爾排序,快速排序,三向快速排序,歸併排序,堆排序)插入排序和歸併排序是穩定的,其餘不穩定。歸併排序不是原地排序,其餘是原地排序,(三向)快速排序空間複雜度為lgN,歸併排序空間複雜度為N,其餘為1。插入排序效率取決於輸入情況,快速排序的效率由概率保證。

l  歸約:為某個問題而發明的演算法正好可以用來解決另一種問題。中位數與順序統計(第k大,可以用快速排序的切分線上性時間內找出第k大的元素)。平均來說基於切分的選擇演算法的執行時間是線性級別的。

l  排序應用一覽:商業計算,資訊搜尋,運籌學,事件驅動模擬,數值計算,組合搜尋。A*演算法。

以下為“答疑”和“練習”中的知識點:

l   

第3章 查詢

3.1 符號表

l  符號表最主要的目的就是將一個鍵和一個值聯絡起來。

l  每個鍵只對應一個值(表中不允許存在重複的鍵)。存入鍵值對和已有鍵衝突則新值替換舊值。鍵不能為空(null),值不能為空(null)。

l  有序符號表,鍵為Comparable物件。

l  有序陣列中的二分查詢,在N個鍵的有序陣列中進行二分查詢最多需要lgN+1次比較,無論是否成功。

以下為“答疑”和“練習”中的知識點:

l   

3.2 二叉查詢樹

l  二叉查詢樹BST是一棵二叉樹,其中每個結點都含有一個Comparable的鍵(以及相關聯的值)且每個結點的鍵都大於其左子樹中的任意結點的鍵而小於右子樹的任意結點的鍵。

l  查詢,插入,遞迴和非遞迴方式,最大鍵和最小鍵,向上取整和向下取整,選擇操作,排名,刪除最大鍵和刪除最小鍵,刪除操作,範圍查詢。

l  在由N個隨機鍵構造的二叉查詢樹中插入操作和查詢命中與未命中的平均所需的比較次數都為~2lnN(約1.39lgN)。(即二叉查詢樹中查詢隨機鍵的成本比二分查詢高約39%)。

以下為“答疑”和“練習”中的知識點:

l  二叉樹檢查,有序性檢查,等值鍵檢查,非遞迴迭代器keys(),按層遍歷(使用Queue)。

3.3 平衡查詢樹

l  我們將一棵標準的二叉查詢樹中的結點稱為2-結點(含有一個鍵和兩條連結),3-結點則是兩個鍵和三條連結。

l  2-3查詢樹:空樹或者由2-結點和3-結點組成的樹。完美平衡的2-3查詢樹中的所有空連線到根節點的距離應該是相同的。

l  查詢,向2-結點中插入新鍵,向一棵只含有一個3-結點的樹中插入新鍵,向一個父節點為2-結點的3-結點中插入新鍵,向一個父節點為3-結點的3-結點中插入新鍵,分解根節點,區域性變換不會影響全域性有序性和平衡性。

l  在一棵大小為N的2-3樹中,查詢和插入操作訪問的結點必然不超過lgN個。

l  紅黑二叉查詢樹:用標準的二叉查詢樹(完全由2-結點構成)和一些額外資訊(替換3-結點)來表示2-3查詢樹。連結分為兩種:紅連結將兩個2-結點連線起來構成一個3-結點,黑臉節則是2-3樹中的普通連結。

l  紅黑樹的性質:所有基於紅黑樹的符號表實現都能保證操作的執行時間為對數級別(範圍查詢除外,它所需要的額外空間和返回的鍵的數量成正比)。一棵大小為N的紅黑樹的高度不會超過2lgN。一棵大小為N的紅黑樹中,根結點到任意結點的平均長度為~1.001lgN。從根節點到所有空連結的路徑上的黑連結的數量相同。

l  平衡樹是由根生長的,每次都有根結點分裂且保持平衡,每次分裂則樹高度加一。

l  由路徑向下的演算法可以用遞迴或迭代,而由路徑向上的演算法一般用遞迴node=operate(node);的方法。

l  刪除演算法:保證當前結點不是2-結點,可以將另一子樹的結點旋轉借一個過來,等刪除後再平衡回來。

以下為“答疑”和“練習”中的知識點:

l  只允許紅色左連結的存在能夠減少可能出現的情況,因此實現所需要的程式碼會少得多。

l  如果按照升序將鍵插入一棵紅黑樹中,樹的高度是單調遞增。如果按照降序將鍵插入一棵紅黑樹中,樹的高度是逐漸遞增然後一下子減下來又逐漸遞增一直迴圈。

l   

3.4 散列表

l  散列表的查詢演算法:第一步用雜湊函式將被查詢的鍵轉化為陣列的一個索引,第二步就是一個處理碰撞衝突的過程(拉鍊法和線性探測法)。

l  雜湊函式和鍵的型別有關,對於每種型別的鍵我們都需要一個與之對應的雜湊函式。一個典型的例子就是社會保險。

l  整數雜湊最常用的方法就是除留取餘法。鍵為0到1之間的浮點數,可以將它乘以M並四捨五入得到一個0到M-1之間的索引值,但這會導致鍵的高位起的作用更大,修正方法則是將鍵表示為二進位制數後再使用除留取餘法。字串也可以當做是整數來處理,組合鍵也可以如此。(Java的hashCode方法)

l  均勻雜湊假設:我們使用的雜湊函式能能夠均勻並獨立地將所有的鍵散佈於0到M-1之間。在一張含有M條連結串列和N個鍵的散列表中(在均勻雜湊假設成立的前提下),任意一條連結串列中的鍵的數量均在N/M的常數因子範圍內的概率無限趨向於1。

l  拉鍊法:將大小為M的陣列中的每個元素指向一條連結串列,每條連結串列中的結點都儲存了散列表為該元素的索引的鍵值對。

l  開放地址散列表:依靠陣列中的空位來解決散列表中碰撞衝突的策略。線性探測法:屬於開放地址散列表,當碰撞發生時(當一個鍵的雜湊值已經被另一個不同的鍵佔用),我們直接檢查散列表中下一個位置(索引值加1)。(動態調整陣列大小來保證使用率在1/8到1/2之間,到達1會無限迴圈)

l  散列表的使用率:α=N/M(N為鍵總數,M為散列表大小)。拉鍊法中α是每條連結串列的長度一般大於1,線性探測表中α是表中已被佔用的空間的比例,不可能大於1。

l  Knuth在1962年的推導:在一張大小為M並含有N=αM個鍵的基於線性探測的散列表中,基於均勻分佈假設,α約為1/2時查詢命中所需要的探測次數約為3/2,未命中所需要的約為5/2。

l  散列表並非包治百病的靈丹妙藥:每種型別的鍵都需要一個優秀的雜湊函式;效能保證來自於雜湊函式的質量;雜湊函式的計算可能複雜而且昂貴;難以支援有序性相關的符號表操作。

以下為“答疑”和“練習”中的知識點:

l  完美雜湊函式:雜湊函式得到的每個索引都不相同即沒有碰撞。

3.5 應用

l  我應該使用符號表的哪種實現:對於典型的應用程式,應該在散列表和二叉查詢樹之間進行選擇。相對於二叉查詢樹,散列表的有點在於程式碼簡單,且查詢時間最優(常熟級別,只要鍵的資料型別是標準的或者簡單到我們可以為它寫出滿足或近似滿足均勻性假設的高效雜湊函式即可)。二叉查詢樹相對於散列表的有點在於抽象結構更簡單(不需要設計雜湊函式),紅黑樹可以保證最壞情況下的效能且它能夠支援的操作更多(如排名、選擇、排序、範圍查詢)。根據經驗法則,大多數程式設計師的第一選擇都是散列表,在其他因素更重要時才會選擇紅黑樹。鍵是長字串時有另外的選擇。(另外注意原始資料型別代替Key型別的節省,重複鍵的處理,Java標準庫的應用)

l  集合用例:過濾器,白名單黑名單。字典類用例:電話黃頁,字典,基因組學,編譯器,檔案系統,DNS。索引類用例:商業交易,網路搜尋,電影和演員。反向索引:網際網路電影資料庫,圖書索引,檔案搜尋。

l  稀疏向量:google的PageRank演算法。

未完待續