《算法图解》笔记与总结
?寫在開頭
這是一篇讀書筆記式的文章,力求簡要地概括《算法圖解》 中陌生和重要的內(nèi)容,所以有的具體內(nèi)容仍需要參考原書。
值得記錄的代碼附在文末,使用python編寫。
持續(xù)更新。
這本書已經(jīng)讀完,這篇筆記也更新至此。不得不說,《算法圖解》是一本對新手非常友好的書,內(nèi)容詳細而不啰嗦,十分有條理,只要沿著順序讀下去,基本能夠很快理解消化。我很慶幸我是在這本書中第一次接觸到諸如動態(tài)規(guī)劃之類的知識點,否則很有可能又被勸退了。當(dāng)然,由于其篇幅限制,很多常用內(nèi)容沒有介紹到,還需要繼續(xù)補充,這些內(nèi)容記錄在我的另一篇筆記里。
第1章 算法簡介
二分查找
大O表示法:O(n),n指操作數(shù)
- 該表示法中的log默認以2為底
- 指出了算法運行時間的增速
- 畫16個格子的例子:一個一個畫,O(n);四次對折,O(logn)
- 指出的是最糟情況下的操作數(shù)
常見的大O運行時間
- O(log n)
- O(n)
- O(n*log n)
- O(n^2)
- O(n!),如旅行商問題
隨著輸入的增加,上述五種算法的操作數(shù)的增加由慢到快
第2章 選擇排序
數(shù)組與鏈表的區(qū)別:“挨著坐”和“分開坐”
數(shù)組在內(nèi)存中是連續(xù)存放的;鏈表中的元素可以存儲在內(nèi)存的任何地方,鏈表的每個元素都存儲了下一個元素的地址。
數(shù)組vs鏈表
- 當(dāng)數(shù)組增加元素而相鄰內(nèi)存單元已被占用,就需要移動整個數(shù)組,鏈表不存在這個問題
- 需要讀取鏈表最后一個元素時,需要從第一個開始依次讀取。當(dāng)需要同時讀取所有元素時,鏈表效率高,需要跳躍時效率低;數(shù)組相反
- 數(shù)組支持隨機訪問,鏈表只能順序訪問
一個疑惑和其解答
Q:在向鏈表的中間插入元素時,不需要先從第一個元素逐個獲取索引,直到要插入的位置?
A:獲取索引即讀的過程,不應(yīng)算在操作數(shù)中;只考慮插入這個操作。
數(shù)組與列表可以組合使用
facebook存儲用戶信息的例子
TODO:選擇排序的例子,為什么是使用平均每次檢查的操作數(shù)的平均值來計算O,而不是直觀理解的階乘?
第3章 遞歸
性能
遞歸并不會比循環(huán)提高性能,但可能更容易理解
組成
- 基線條件:控制何時停止調(diào)用自己,避免無限循環(huán)
- 遞歸條件:循環(huán)調(diào)用自己
棧
只對最上層元素執(zhí)行兩種操作:插入、刪除并讀取(彈出)
調(diào)用棧
例子的文字總結(jié):一個函數(shù)A中調(diào)用了另一個函數(shù)B,當(dāng)A開始執(zhí)行,內(nèi)存分配給其一部分,存儲其涉及到的所有變量;當(dāng)執(zhí)行到B,內(nèi)存分配給B一部分,并在棧中壓在A的部分之上;執(zhí)行B,其被從棧上彈出,此時A位于最上方,故繼續(xù)執(zhí)行A。這個用于儲存多個函數(shù)的變量的棧,被稱為調(diào)用棧。
調(diào)用另一個函數(shù)時,當(dāng)前函數(shù)暫停并處于未完成狀態(tài)。
遞歸調(diào)用棧:以階乘為例
要注意,一個變量在每次調(diào)用中的值可能不同,在一個調(diào)用中不能訪問另一個調(diào)用中的變量(還是比較符合常識的)
棧的弊端
每次函數(shù)調(diào)用會占用內(nèi)存,棧過高占用的內(nèi)存也過多
解決辦法:改用循環(huán);使用尾遞歸(本書不涉及)
第4章 快速排序
分而治之,Divide and Conquer,D&C
- 找出盡可能簡單的基線條件
- 不斷將問題分解直到滿足基線條件
Tip:涉及到數(shù)組的遞歸函數(shù)常見的基線條件是數(shù)組為空或只含一個元素。
快速排序思路
- 最簡單的排序:不需要排序,即數(shù)組中只有0或1個元素
- 兩個元素的數(shù)組,比較二者的值
- 多于兩個元素的數(shù)組,分而治之
- 選取一個元素作為基準值(暫取第一個)
- 分區(qū):遍歷,找出比基準值大的和小的元素,分別構(gòu)成兩個數(shù)組。目前有:比基準值小的子數(shù)組、基準值、比基準值大的子數(shù)組
- 對子數(shù)組遞歸,直到剩下的數(shù)組長度小于等于二
- 子數(shù)組排序后,合并
????????使用python實現(xiàn)的快排,見代碼1
再談大O表示法 - 比較合并排序和快速排序 -?大O表示法中的常量
例子,逐個打印數(shù)組元素的函數(shù),一個沒有sleep(1)(記一次sleep的時間為c),另一個有,則其運行時間分別為c*n和n。但是在大O表示法中,固定時間量,也即常數(shù)c,是忽略不計的,因為一般來說對時間影響更大的是n和logn的區(qū)別。
對于運行時間都為nlogn的快速查找和合并查找,常量的影響就可能很大;快速查找更快,因為其遇上最糟情況的可能性比平均情況低得多。
平均情況和最糟情況
以快速排序的基準值為例,當(dāng)基準始終選擇在開頭時(最糟情況),調(diào)用棧會非常長,O(n);而當(dāng)基準選擇在中間(最佳情況),調(diào)用棧就短得多,O(log n)。
取一個元素為基準值,并劃分數(shù)組的操作,不論基準值取在了哪里,劃分了多少組,都涉及到了n(數(shù)組長度)個元素,即每次調(diào)用棧的操作時間都為O(n)。
最佳情況的層數(shù)O(log n),每層操作時間O(n),所以總時間O(nlog n);最糟情況層數(shù)O(n),所以總時間O(n^2)。
只要每次的基準值都是隨機選擇,快速排序的平均運行時間就是O(nlog n);也就是說,最佳情況也是平均情況?-?這里不求甚解了:(
第5章 散列(Hash)表
散列函數(shù)
將輸入映射到數(shù)字
- 必須:一致性:對同樣的輸入,映射的數(shù)字必須相同
- 理想但不必須:對于不同的輸入,映射到不同的數(shù)字
Maggie的例子
創(chuàng)建一個用于存儲物價的空列表
蘋果->散列函數(shù)->輸出一個數(shù)字->列表中這個數(shù)字的位置存儲著蘋果的價格
該例子中散列函數(shù)的性質(zhì):
- 輸入相同,輸出就相同
- 輸入不同,輸出就不同
- 只返回有效的輸出,如列表長度為5,就不會返回索引為100
散列表:散列函數(shù)+數(shù)組
一種包含額外邏輯的數(shù)據(jù)結(jié)構(gòu),由鍵和值組成。
數(shù)組和鏈表都被直接映射到內(nèi)存,但散列表使用散列函數(shù)來確定元素的存儲位置。
散列表獲取元素的速度和數(shù)組一樣快。
Python的字典就是散列表。
散列表的應(yīng)用
- 查找,eg:電話薄
- 避免重復(fù),eg:投票
- 緩存,eg:Facebook,當(dāng)URL在散列表中時,發(fā)送緩存中的數(shù)據(jù),否則讓服務(wù)器處理。如此加快了加載速度,并減輕了服務(wù)器負擔(dān)
沖突?
存儲apple和avocardo的價格的例子:能夠總是將不同的鍵映射到不同值的散列函數(shù)難以實現(xiàn)。
不同的鍵被分配給了同一個值,即沖突。
最簡單的解決辦法,在該位置創(chuàng)建鏈表,依次存儲,但是性能不佳。
理想的情況是,散列函數(shù)將鍵均勻映射到散列表的不同位置。一個好的散列函數(shù)很重要。
常量時間O(1)
散列表在平均情況下的操作時間為O(1),不意味著馬上,而是不論散列表多大,所需時間都相同。
散列表的性能
平均情況下,散列表的查找(獲取給定索引處的值)速度與數(shù)組一樣快,而插入和刪除速度與鏈表一樣快,兼具兩者的優(yōu)點。但在最糟情況,即有沖突的情況下,散列表的各種操作的速度都很慢。
選讀:散列表的實現(xiàn) 避開最糟情況
避免沖突的方式
- 較低的填裝因子:元素數(shù)/位置總數(shù),越低越不容易沖突
- 良好的散列函數(shù):讓數(shù)組中的值均勻分布
第6章 廣度優(yōu)先搜索 圖 樹
廣度優(yōu)先搜索:尋找解決問題的最短路徑的問題,用于圖的查找算法
圖
圖由節(jié)點和邊組成。一個節(jié)點可能與眾多節(jié)點直接相連,稱為鄰居。
兩類問題
- 從節(jié)點A出發(fā),有前往節(jié)點B的路徑嗎?eg:朋友中有無芒果銷售商
- 從節(jié)點A出發(fā),前往節(jié)點B的哪條路徑最短?eg:朋友中哪個芒果銷售商關(guān)系最近
?在名單中依次檢查,如果當(dāng)前人(一度關(guān)系)不是銷售商,就把他的朋友加入到相應(yīng)的關(guān)系部分(二度關(guān)系)(隊列的末尾)。實現(xiàn)“依次”,需要數(shù)據(jù)結(jié)構(gòu):隊列。如果不是依次的,找到的就可能不是最短路徑。
隊列
隊列,先進先出,First In First Out,FIFO
棧,后進先出,Last In First Out,LIFO
在python中
- 創(chuàng)建雙端隊列:q =?deque()
- 向隊列添加元素:q += item(可以是數(shù)組以一次添加多個)
- 彈出第一個元素:item = q.popleft()
使用散列表實現(xiàn)圖
找銷售商的例子,創(chuàng)建一個字典,以graph["you"] = ["alice", "bob", "claire"]、graph["alice"] = ["peggy"]的形式添加,即以一人為鍵,其下級關(guān)系的所有人的數(shù)組為值。由于散列表是無序的,所以添加內(nèi)容的順序也沒有影響。
有向圖:關(guān)系是單向的,eg:有從別人指向Anuj的箭頭,但沒有從Anuj指向別人的箭頭,所以Anuj沒有鄰居
無向圖:沒有箭頭,直接相連的節(jié)點互為鄰居
在銷售商例子中,由于一個人可能同時是多個人的朋友,為了避免重復(fù)檢查或無限循環(huán),在檢查完一個人后,應(yīng)將其標記為已檢查,且不再檢查他。
運行時間
在整個人際關(guān)系網(wǎng)中搜索芒果銷售商,意味著將沿每條邊前行,因此運行時間至少為O(邊數(shù));
使用了一個隊列,將一個人添加到隊列需要的時間是固定的,O(1),因此總時間為O(人數(shù));
所以,廣度優(yōu)先搜索的運行時間為O(V+E),其中V為頂點數(shù),E為邊數(shù)。
樹
一種特殊的圖,其中沒有往后指的邊。
如果任務(wù)A依賴于任務(wù)B,在列表中任務(wù)A就必須在任務(wù)B后面。這被稱為拓撲排序,使用它可根據(jù)圖創(chuàng)建一個有序列表。
合理的順序:
- 起床 - 刷牙 - 吃早餐 - 洗澡
- 起床 - 刷牙 - 洗澡 - 吃早餐(即吃早餐必須在刷牙后,但不一定緊挨著)
第7章?狄克斯特拉算法
加權(quán)圖:帶權(quán)重的圖。否則是非加權(quán)圖。
例子:由起點到終點,每一段都有相應(yīng)的時間(權(quán)重)。廣度優(yōu)先搜索找出的是段數(shù)最少的路徑,狄克斯特拉可用于找出最快(總權(quán)重最小)的路徑。換言之,廣度優(yōu)先搜索找出的是非加權(quán)圖的最短路徑,狄克斯特拉找出的是加權(quán)圖的最短路徑。
適用范圍:沒有負權(quán)邊的有向無環(huán)圖
步驟
環(huán)
從某節(jié)點走一圈后又回到該節(jié)點。繞環(huán)的路徑不可能是最短路徑。
無向圖意味著兩個節(jié)點彼此指向?qū)Ψ?#xff0c;其實就是環(huán),在無向圖中,每條邊都是一個環(huán)。
狄克斯特拉算法只適用于有向無環(huán)圖。
負權(quán)邊
即權(quán)重為負的邊。
狄克斯特拉算法假設(shè):對于處理過的海報節(jié)點,沒有前往該節(jié)點的更短路徑。 這種假設(shè)僅在沒有負權(quán)邊時才成立。在琴譜換鋼琴的例子中,已經(jīng)更新過經(jīng)由海報的路徑,但如果有負權(quán)邊,就相當(dāng)于找到了前往海報的更短路徑,而在狄克斯特拉算法中,經(jīng)由海報的路徑已經(jīng)更新并不再改變,所以無法正常更新。
不能將狄克斯特拉算法用于包含負權(quán)邊的圖。可以用貝爾曼-福德算法(略)。
代碼實現(xiàn)
需要三個散列表和一個數(shù)組,用于:
- graph:記錄鄰居關(guān)系和權(quán)重(兩層)
- costs:更新開銷(從起點到該節(jié)點的總權(quán)重)
- parents:更新父節(jié)點
- processed = []:記錄已經(jīng)處理的節(jié)點
代碼實現(xiàn)見代碼2
第8章 貪婪算法
有些情況下,完美是優(yōu)秀的敵人
排課問題與背包問題
一間教室,課的時間有沖突,選出盡可能多且時間不沖突的課程。
- 選出結(jié)束最早的課作為要在這間教室上的第一堂課
- 選擇第一堂課結(jié)束后才開始的課。同樣選擇結(jié)束最早的課作為第二堂課,如此重復(fù)
每步都選擇局部最優(yōu)解,最終得到的就是全局最優(yōu)解
但同樣的思路不適用于另一個例子,背包問題:容量35的背包,要裝下價值最大的東西,可以裝的有重量30價值3000的音響、重量20價值2000的筆記本、重量15價值1500的吉他。如果按照上面的思路,先裝入最值錢的音響,就無法再裝入別的東西,價值比筆記本+吉他少。
集合覆蓋問題
例子:需要讓節(jié)目被全美50個州的聽眾都收聽得到,在每個廣播臺播出都需要支付費用,因此力圖在盡可能少的廣播臺播出。每個廣播臺都覆蓋特定的區(qū)域,不同廣播臺的覆蓋區(qū)域可能重疊。即需要找出覆蓋全美50個州的最小廣播臺集合。
窮舉法列出所有可能的集合,子集有2**n個。
需要使用近似算法:
- 選出覆蓋了最多的未覆蓋州的一個廣播臺(不考慮它覆蓋了多少已覆蓋的)
- 重復(fù)直到覆蓋所有州
該例子的python代碼,見代碼3
NP完全問題
簡單定義是,以難解著稱的問題,如旅行商問題和集合覆蓋問題。有觀點認為不可能編寫出可快速解決NP完全問題的算法。
如果能判斷出一個問題是不是NP完全問題,就可以決定是否采用貪心算法。不存在判斷標準,但可以根據(jù)問題的特征判斷:
- 元素較少時算法的運行速度非常快,但隨著元素數(shù)量的增加,速度會變得非常慢
- 涉及“所有組合”的問題
- 不能將問題分成小問題,必須考慮各種可能的情況
- 涉及序列(如旅行商問題中的城市序列)且難以解決
- 涉及集合(如廣播臺集合)且難以解決
- 可轉(zhuǎn)換為集合覆蓋問題或旅行商問題,那它肯定是NP完全問題
第9章 動態(tài)規(guī)劃
目的:將某個指標最大化。
背包問題,窮舉太復(fù)雜,貪心算法可能找出的不是最優(yōu)解。需要使用動態(tài)規(guī)劃。
動態(tài)規(guī)劃先解決子問題,再逐步解決大問題。
以背包問題為例介紹動態(tài)規(guī)劃
背包容量4磅,音響3000美元4磅,筆記本電腦2000美元3磅,吉他1500美元1磅。
每個動態(tài)規(guī)劃算法都從一個網(wǎng)格開始。表格列標題為容量(不同容量的子背包),行為可選擇的商品,每個格子用于記錄當(dāng)前能夠裝下的最高價值和其對應(yīng)的組合。每一行的格子考慮當(dāng)前行所代表的商品和當(dāng)前行以上的商品,如:第一行,就只能裝吉他;第二行只能裝音響和吉他,不能考慮筆記本電腦。
?目的是讓背包中商品的價值最大,計算每一行時,該行都表示的是當(dāng)前的最大價值。
更新到第一行時,最大價值是吉他1500美元。
更新完第一行后的最大價值 更新完第二行后的最大價值 標題?更新完第二行后的最大價值?在最后一個單元格,如果偷單價最高的音響,則3000美元;但如果選擇筆記本電腦(當(dāng)前行所讀應(yīng)的),則2000美元,剩下的1磅空間再偷吉他,總共3500美元。
其實在每一個單元格,都使用了如下公式計算價值,對應(yīng)上圖的紅色框。
?注意cell[i-1][j-當(dāng)前商品重量]中的[i-1]而不是[i]因為,i代表本行商品,已經(jīng)裝入。
特性
- 在不改變表格列的粒度時,增加商品,不需要重新計算表格,往下繼續(xù)算即可;
- 行的排列順序不影響最終結(jié)果;
- 每列從上到下,價值不可能減小;
- 要么考慮拿走整件商品,要么考慮不拿,而沒法判斷該不該拿走商品的一部分(拿一部分應(yīng)該用貪心);
- 僅當(dāng)每個子問題都是離散的,即不依賴于其他子問題時,動態(tài)規(guī)劃才管用(旅行規(guī)劃為例);
- 大背包至多含有兩個子背包,但子背包可能又含有子背包;
- 最優(yōu)解可能出現(xiàn)在背包沒裝滿的情況;
以尋找最長公共子串為例應(yīng)用動態(tài)規(guī)劃
最長公共子串要求在原字符串中是連續(xù)的,而子序列只需要保持相對順序一致,并不要求連續(xù)。
用戶輸入HISH,備選單詞FISH、VISTA
Tips:
- 每種動態(tài)規(guī)劃解決方案都涉及網(wǎng)格;
- 單元格中的值通常就是要優(yōu)化的值。在前面的背包問題中,單元格的值為商品的價值;
- 每個單元格都是一個子問題,因此應(yīng)考慮如何將問題分成子問題,這有助于找出網(wǎng)格的坐標軸
?對于尋找最長公共子串問題
- 單元格中的值即需要優(yōu)化的值:最長公共子串的長度
- 橫坐標,輸入單詞
- 縱坐標,可能匹配的單詞
- 逐行計算,當(dāng)cell[i][j]的i和j對應(yīng)的字母不同,則該單元格為0;當(dāng)相同,該單元格為1+cell[i-1][j-1]
- 整個表格填充完后尋找表格中的最大值
對于尋找最長公共子序列問題
- 當(dāng)兩字母相同時,值為左上角加1,這點比較好理解
- 當(dāng)兩字母不同時,值為上方和左側(cè)中值大的,這是為了保存當(dāng)前已經(jīng)尋找到的最長子序列的長度,如此才能使下一次找到兩個相同字母時,其左上角的值是正確的
?沒有放之四海皆準的計算動態(tài)規(guī)劃解決方案的公式.
第10章 KNN
非常簡要地介紹了KNN、推薦系統(tǒng)、OCR等,因為已經(jīng)了解且這里介紹的太基礎(chǔ),所以不詳細記錄。
第11章 What's next
這一章也語焉不詳,不作記錄。
樹:B樹,紅黑樹,堆,伸展樹
反向索引;傅里葉變換;并行算法,mapreduce;概率型算法-布隆過濾器和HyperLogLog;安全散列算法SHA;Diffie-Hellman密鑰;線性規(guī)劃。
代碼
代碼1 Python實現(xiàn)快排
def q_sort(l):if len(l) == 2:if l[0] > l[1]:return [l[1], l[0]]else:return lelif len(l) == 1 or len(l) == 0:return lelse:base_num = l[0]bigger_l = []smaller_l = []for i in l:if i < base_num:smaller_l.append(i)elif i > base_num:bigger_l.append(i)return q_sort(smaller_l) + [base_num] + q_sort(bigger_l)代碼2? 使用狄克斯特拉算法找到權(quán)重最短的路徑和其權(quán)重值
# 創(chuàng)建圖 graph = {} graph['Start'] = {} graph['Start']['A'] = 5 graph['Start']['B'] = 0 graph['A'] = {} graph['A']['C'] = 15 graph['A']['D'] = 20 graph['B'] = {} graph['B']['C'] = 30 graph['B']['D'] = 35 graph['C'] = {} graph['C']['End'] = 20 graph['D'] = {} graph['D']['End'] = 10inf = float("inf")cost = {} parents = {} processed = [] # 原本是遍歷graph的鍵,將其作為cost的鍵,并將值都設(shè)為inf,但是這種初始化不方便程序開始,所以手動初始化第一步的鄰居 cost['A'] = 5 cost['B'] = 0 cost['C'] = inf cost['D'] = inf cost['End'] = inf parents['A'] = 'Start' parents['B'] = 'Start' parents['C'] = None parents['D'] = None parents['End'] = None# 尋找開支最小的節(jié)點 def find_min_node(cost, processed):temp = max(cost.values())node = Nonefor k in cost:if k not in processed and cost[k] <= temp:node = ktemp = cost[k]return node if node else None #都處理過就返回None# 算法 Node = find_min_node(cost=cost, processed=processed) # 尋找最便宜節(jié)點 while Node:if Node == 'End':breakfor k in graph[Node]:temp_cost = cost[Node] + graph[Node][k]if temp_cost < cost[k]:cost[k] = temp_costparents[k] = Nodeprocessed.append(Node)Node = find_min_node(cost=cost, processed=processed)def p_path(e): # 遞歸打印路徑,因為時間倉促,沒有仔細研究邊界條件,所以不會打印最后的‘End’if e in parents.keys():print('^\n' + parents[e])return p_path(parents[e])print('*'*10 + "\nCost:{c}\n".format(c=cost['End']) + '*'*10) print('End') p_path('End')''' 結(jié)果: ********** Cost:35 ********** End ^ D ^ A ^ Start'''代碼3 使用貪婪算法解決集合覆蓋問題
states = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] # 州 r = {} # 廣播臺 r['r1'] = ['A', 'C', 'F'] r['r2'] = ['B', 'C', 'D'] r['r3'] = ['C', 'E', 'G'] r['r4'] = ['A', 'G']s = set(states) covered = set() r_selected = []while s - covered: # 還有未覆蓋的k_selected = Nonel_temp = 0for k in r: # 尋找能覆蓋最多未覆蓋地區(qū)的頻道if k not in r_selected:temp = (s - covered) & set(r[k]) # 該頻道能覆蓋的未覆蓋地區(qū)數(shù)if len(temp) > l_temp:l_temp = len(temp)k_selected = kcovered = covered | set(r[k_selected])r_selected.append(k_selected) print(r_selected)總結(jié)
以上是生活随笔為你收集整理的《算法图解》笔记与总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HDU 3435 KM A new Gr
- 下一篇: 敏捷软件开发:原则、模式与实践——第12