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

算法第四版(謝路雲譯)

前言:這本書是算法和數據結構方面的經典書籍,使用java語言描述,強力推薦好好看看這本書,對於考研和找工作都極其有幫助。

官方網站:http://algs4.cs.princeton.edu/home/有部分源代碼和部分課後習題答案。

個人練習代碼:https://github.com/morefans/AlgorithmsFourthEdition

第1章 基礎

1.1 基礎編程模型

l  Java程序的基本結構;原始數據類型與表達式;語句;簡便記法;數組;靜態方法;API;字符串;輸入輸出;二分查找。

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

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

1.2 數據抽象

l  使用抽象數據類型;抽象數據類型舉例;抽象數據類型的實現;更多抽象數據類型的實現;數據類型的設計。

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  以下爲“答疑”和“練習”中的知識點:

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  以下爲“答疑”和“練習”中的知識點:

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  以下爲“答疑”和“練習”中的知識點:

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

2.2 歸併排序

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

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

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

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

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

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

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  以下爲“答疑”和“練習”中的知識點:

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  以下爲“答疑”和“練習”中的知識點:

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  以下爲“答疑”和“練習”中的知識點:

l   

第3章 查找

3.1 符號表

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

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

l  有序符號表,鍵爲Comparable對象。

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

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

l   

3.2 二叉查找樹

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

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

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

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

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  如果按照升序將鍵插入一棵紅黑樹中,樹的高度是單調遞增。如果按照降序將鍵插入一棵紅黑樹中,樹的高度是逐漸遞增然後一下子減下來又逐漸遞增一直循環。

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  以下爲“答疑”和“練習”中的知識點:

l  完美散列函數:散列函數得到的每個索引都不相同即沒有碰撞。

3.5 應用

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

l  集合用例:過濾器,白名單黑名單。字典類用例:電話黃頁,字典,基因組學,編譯器,文件系統,DNS。索引類用例:商業交易,網絡搜索,電影和演員。反向索引:互聯網電影數據庫,圖書索引,文件搜索。

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

 


未完待續

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章