這陣子看了兩本算法書,《算法》和《算法導論》。java
前一本讀着很輕鬆,內容基本與大學數據結構課程重疊,示例代碼用java編寫,學習曲線平緩,對應用程序員來講,讀它就挺好。程序員
後一本我是邊看麻省理工的《算法導論》公開課邊讀的,力不從心,由於我數學基礎很差(詳下),若是不看數學證實,其內容跟前一本就差很少了,數學基礎比較好、對算法感興趣的朋友,能夠嘗試之。算法
強烈建議各公開課平臺的學習資源,質量很是高,相見恨晚!足不出戶,就能學習名校的課程。但學習時別忘了跟着作習題、作筆記、預習複習。編程
下面是個人《算法》筆記。數組
零、思想數據結構
重中之重。算法真正的精髓。內功心法所在。相比之下,後面講到的都是具體招式。數據結構和算法
內功心法的意義就在,有朝一日你遇到一個沒有既有經驗的問題,能本身設計出適當的數據結構和算法,而不是隻會用別人的輪子。函數
算法的根本思想,是大問題化爲多個小問題,逐漸解決之。(《孫子兵法》:治衆如治寡,分數是也;鬥衆如鬥寡,形名是也。)工具
1.分治學習
若是能將大問題拆爲同性質的小問題,當小到必定程度,能直接解決之。
則採用分治法,遞歸解決。
2.貪心
若是爲小問題找到的最優解,能經過增長一點步驟,解決一個大一點的問題,並依然是最優解。
則採用貪心法,從0開始,逐步蠶食掉大問題。
3.動態規劃
若是用分治法拆出的小問題中常見到相同問題,重複計算它們,成本過高。
則採用備忘法,計算完每一個小問題的結果後,記錄之,下次見到同一小問題,再也不重複計算。
1、數學基礎
《算法導論》在附錄中給出了4項必備的數學基礎。
我看了以後,很羨慕考研的同窗,他們這幾門課都很紮實。
這4門數學除了是考研數學科目、算法的基礎外,其實更是觀察世界、指導生活(不只僅是生產!)的工具,我決定要以看公開課的方式,逐步從新學之。
1.微積分
2.離散數學
3.機率論
4.線性代數
2、線性表
1.按實現分類
順序表:隨機訪問方便,插入刪除成本高,由於一側的元素要「噠噠噠」依次移動一格。
鏈表:隨機訪問麻煩,由於要從頭「一二三」數過去,插入刪除方便,將特定兩三個節點的指針「咔吧」從新接一下就行了。
2.按做用分類
棧:後進先出,直觀理解就是坐電梯。用處主要在處理大問題時,細化爲小問題,小問題解決完再繼續處理大問題的時候。計算機的函數調用的核心數據結構。若是不幸小問題沒完沒了,如無限遞歸,則stackoverflow。
隊列:先進先出,講「先來後到」的數據結構。典型使用就是「任務隊列」,先到的先處理。若是容許「插隊」,就是「優先級隊列」了,讓領導先走!
3、排序
假設有一個初始隊列4132,目標是從小到大排序成1234。
1.冒泡排序
最簡單也最慢的算法,慢到很多書都不講了。
思想是讓最後一個元素盡力向上爬,碰見比它大的就壓過之(交換次序),若是遇到比它小的就停下,由小的繼續往上爬。爬到第一個元素時,就保證了第一個元素是所有元素中最小的一個。
再也不考慮第一個元素,重複這一過程。4132,1/423,12/43,123/4。
2.選擇排序
每次都從隊列中選出一個最小的元素,放到「已排序隊列」裏,直到選完。
/4132,1/432,12/43,12/34。
3.插入排序
每次都從「未排序隊列」中選第一個元素,插到「已排序隊列」的適當位置,直到插完。
我玩撲克時理牌就用這法。
4/132,14/32,134/2,1234
4.希爾排序
先把「相距較大間距」的元素們排序一遍,再適度減少間距再排一遍,最後一次相距零間距排。
4132,先間隔較大間距(這個簡陋例子,姑且間隔1),4與3排序,1與2排序,得3142,再間隔較小間距(本例中直接到0了)排序。
5.快速排序
最被喜歡的排序,既容易理解,又快。
選一個數字爲軸,比它小的放左邊,比它大的放右邊,而後對其兩邊各進行這一變化便可。
4132,第一步選4,第二步選1,都是把所有剩餘元素放到同側,不划算,因此隨機選擇軸元素很關鍵。
假設第一步選了3,12放左側,4放右側,右側完結,左側繼續,假設選2爲軸,1放左側,右側沒有,即得結果1234。
6.歸併排序
所謂歸併,好比一班和二班已經各排成了從矮到高的有序隊列,想歸併成一隊,怎麼辦?比較一班和二班的排頭,更矮的選出來站到「結果隊列」裏,該班的新排頭再跟對方排頭比,以此類推。
歸併成一個新隊列後,再跟別的隊列歸併,直到通通歸併爲一隊。
14/23 -> 1|4/23 -> 12|4/3 -> 123|4 -> 1234
7.堆排序
所謂堆,就是一棵每一個父節點數值都大於其一切子孫節點數值的徹底二叉樹,若是看不懂,先跳到下面看樹那節。
爲方便說(放圖太麻煩了,我相信純文字能說清),此次我們從大到小排序,道理固然是同樣的。
分兩步,一是從無序隊列構造樹,二是從樹輸出有效隊列,結合起來,就是從無序隊列,獲得了有序隊列。
第一步,拿一個新節點,放到原樹的最後一個位置,而後使之盡力向上冒泡,直到冒不動爲止,再拿下一個新節點。如處理4132:
4 -> 4左下1 -> 4左下1右下3 -> 4左下1右下3,1左下2 -(2冒泡)-> 4左下2右下3,2左下1
第二步,取出根節點放到結果隊列,把最後一個節點僭越到根位置,而後考察其兩個葉子節點,若是有更有實力的,則向最有實力的退位讓賢,交換位置,到新崗位後繼續考察下屬能力,直到降到本身勝任的位置。
4左下2右下3,2左下1 -(取出4,1上位)-> 1左下2右下3 -(3篡位)-> 3左下2右下1 -(取出3,1上位)-> 1左下2 -(2篡位)-> 2左下1 -(取出2)-> 1孤家寡人 -(取出1)-> 結果4321
4、樹
若干節點,及若干鏈接節點的無向邊,構成的圖,若是從每一個節點出發,都能到達任意一個節點(強連通),同時圖中不存在環,即稱爲樹。
數據結構中說的樹,在此基礎上再嚴格點,分爲若干層。如某團長手下有若干連長,每一個連長手下有若干班長,畫組織結構圖,把直接領導和直接下屬鏈接起來。直接領導稱爲父節點,如團長是連長的父節點,連長是其屬下每一個班長的父節點;直接下屬稱爲子節點;最高長官稱爲根節點,即沒有父節點的節點;沒有子節點的節點稱爲葉子節點,好比《士兵突擊》裏鋼七連只剩老七(連長)和三多(班長)的時候,三可能是葉子節點,若是這時三多被老A挖走(實際劇情是老七先走,不贅),則老七堂堂連長,將不幸成爲葉子節點。
1.二叉樹
若是樹的每一個父節點最多隻有兩個子節點,該樹即稱爲二叉樹。
最典型用途是二叉查找,即若是每一個根節點數值都比其左子樹(此概念望文生義,不贅)任意節點數值更大,比其右子樹的任意節點數值更小,該樹即稱爲二叉排序樹。在想尋找某個數值時,能夠先將該數值與根節點比,若是比根節點大,則去與其右子樹根節點比,若是比根節點小,則去與其左子樹根節點比,直到找到或比到葉子節點爲止。
好像「猜數字」遊戲,一本好書價值1元到256元以內,假設你蠢到看不出那書不可能1元,又聰明到知道採用二叉查找,則該先猜128元,根據主持人說「高了」「低了」再決定猜64仍是192元,假如64仍是「高了」,則該繼續猜32,以此類推。
2.紅黑樹
強烈推薦看《算法》中紅黑樹的章節,從2-3樹開始講,講到紅黑樹,學習曲線很是平滑。
二叉樹和紅黑樹都有着清晰的用於保持樹平衡的邏輯,限於篇幅很少講。平衡的目的是保持最差狀況下的查找程度爲logN(以2爲底,總節點數的對數)。
3.散列表
這個不是樹……但在邏輯上放在這裏最好,由於都是用於「查找」的數據結構。
編程中最基本的兩種數據結構,list和map,list就是上文說的線性表,核心是有順序,map就是這裏說的散列表(hashmap)或樹(treemap),核心是鍵值對。set呢,多以map作底層結構。
樹上面說了,這裏說散列表。
散列表又叫字典,從這字面可知,主要用途是查找,經過鍵(key)查找值(value),首先,要把鍵算成一個數字,這算法(散列函數)就是java裏類們都要實現的hashcode方法。
而後,有兩種可選的實現方式,一是連接法,首先有個主list裝着鍵們,好比123,而後1對應一個list,2對應一個list,3對應一個list,我把鍵算出是1以後,就到1對應的list元素裏去找元素。顯然,1對應的list長度越短,查找時間越短,那彷佛主list的鍵們越多越好,好比1到30,那每一個子list的長度將變爲原長度的十分之一,但主list的鍵們也不能太多,好比1到30000,那大部分鍵對應的實際上是空,裏面一個元素都沒有,就浪費空間了,因此要權衡。
另外一種實現方式是散列法,不存在次list,把鍵算成數字後,直接往主list對應的位置存,若是該位置已經被別的元素佔了(碰撞)怎麼辦?則用某種算法,算出一個新數,再嘗試往新位置放(再散列),以此類推。顯然,這種方法,主list的長度也很關鍵,若是過短,則碰撞太多,插入、查找都麻煩,若是太長,又浪費地方。
對兩種方法,散列函數都很是重要,若是全部元素的散列函數都計算出同一個值,散列表就毫無心義了。
5、圖
終於到圖了!這是我最喜歡的部分,好玩,並且熟悉,由於我認識一個同窗,數據結構年年掛,直到大四,因此我每一年都陪他複習備考一次,老師考卷中的高分的題目,就是考圖的幾個算法,迪傑斯特拉,普里姆,克魯斯卡爾……好懷念那段日子。
什麼是圖?一堆節點,及若干邊,就構成了圖,根據線路是否有方向性,分爲有向圖、無向圖,根據圖中是否有環,分爲有環圖、無環圖。
若是任意兩個節點都有線路鏈接,則稱爲強連通圖,這裏主要聊的都是這種。
1.深度優先、廣度優先遍歷
A連B,B連C,B連D,C連E
想把圖裏全部節點都研究一遍,怎麼辦?
從一個節點開始,走向下一個節點,而後再走向下一個節點的下一個節點,直到「眼前無路想回頭」,就是「深度優先遍歷」。在例子裏,從A到B,從B到C,從C到E,E回頭到C,C回頭到B,從B到D,完成遍歷。
「廣度優先遍歷」則是,從一個節點開始,先扇面狀把能到的節點都掃一遍(廣嘛),而後再「移步」到子節點們。在例子裏,從A到B,從B到C,從B到D,從C到E,完成遍歷。
2.最小生成樹
若是樹的每條邊都有個權值,要求選出總權值最小的一些邊,讓全部節點都能互相連通,怎麼辦?
這就是「最小生成樹」問題,顯然,選出的結果確定是一棵樹,樹的兩個條件,連通和無環,連通沒必要講,爲何無環?若是有環,去掉任意一條邊,節點們依然相互連通,權值變小了,由此反證,必然無環。
解此問題,兩種方法,一是普里姆算法,二是克魯斯卡爾算法,好專業,終於有點算法的感受了。
其實都很簡單,先說普里姆,將圖劃爲「已解決」和「未解決」兩個部分,一開始「已解決」爲空,「未解決」是全集,首先,任選一個點放到「已解決」,而後考察全部從「已解決」連向「未解決」的邊,哪條最短,就把該邊連向的「未解決」節點歸入「已解決」集合,如此反覆,直到所有節點都「已解決」,結果就是最小生成樹。
再說克魯斯卡爾,一開始,把每一個節點視爲一個「份量」,尋找所有「鏈接兩個不一樣份量的邊」中最短的一條,將其鏈接的兩個份量合併爲一個份量,並重復這一行爲,直到所有節點都屬於同一份量,結果就是最小生成樹。
3.單源最短路徑
最小生成樹研究的是無向圖,最短路徑則研究有向圖。
顧名思義,最短路徑是要求給出從某地到另外某地的最便捷走法,這有兩個算法,一是迪傑斯特拉算法,二是貝爾曼福特算法。
先說迪傑斯特拉,這是相似普里姆的「貪婪算法」,從一個極小的「已解決」開始,逐步蠶食,直到將「未解決」吃掉。差異在這回每一個節點上都有個數字,表明其到起點的距離,每次都選所有「從已解決到未解決的邊」指向的「未解決」節點中,數字最小的一個。好比起點是家,家距離學校500米,家距離市場600米,學校距離公園200米,根據算法,把學校標註500,市場標註600,500小於600,把學校歸入「已解決」,學校進入「已解決」以後,公園也變成了從「已解決」能夠到達的「未解決」的節點,用學校的500加200,公園標註爲700,與市場的600米相比,600較小,把市場歸入「已解決」。注意,這個步驟,若是是普里姆算法,歸入的是公園而非市場,由於普里姆不存在起點,都連上就ok,只考慮哪條邊最短,但迪傑斯特拉有起點,要比較的是「從起點出發直到這裏的距離」。
再說貝爾曼福特,這算法是把每一個節點賦予一個表示「距離起點有多遠」的數值,除了起點外,其餘起點一開始都是無窮大,而後依次考察每條邊,若是這條邊能讓指向的節點的數值變小,就將該節點數值變小(收縮),把全部邊都考察一遍以後,若是這一遍中執行了「收縮操做」,則再把全部邊都考察一遍,直到某一遍徹底沒收縮爲止。上面的例子,好比考察次序是200-500-600,則第一遍200啥用沒有,500將學校的+∞變爲500,600將市場的+∞變爲600,這遍進行了收縮,再來一遍,這回200有用了,將公園的+∞變爲700,500和600都沒有,這遍也收縮了,再來一遍,200/500/600都沒用,完結。
6、字符串
字符串本質是字符數組。說到算法,最常說的就是字符串查找的KMP算法。
好比想從「子龍子龍世無雙」裏尋找「子龍子翼」,「子」等於「子」,「龍」等於「龍」,「子」等於「子」,「龍」不等於「翼」,糟糕!這時源文本的「已搜索序列」的「後綴們」有「子龍子龍」「龍子龍」「子龍」「龍」四個,而目標文本的「前綴們」有「子龍子翼」「子龍子」「子龍」「子」四個,考察兩個「們」,發現有個相同元素,即「子龍」,長度爲2(若是有多個相同元素,則取最長的),意味着源文本的「已搜索序列」中最後兩個字與目標文本的前兩個字相同,下次用源文本的下一個字「世」與目標文本的第三個字「子」相比便可。若是是樸素算法,則要用「子龍子龍世無雙」的第一個「龍」字與「子龍子翼」的第一個「子」字相比,KMP算法比樸素算法快多了!
7、寫在後面
算法的水太深,我僅僅入了個門,回頭看看,上面所寫,全是大學《數據結構》的內容,長嘆一聲。
也有更深刻學習的意願,但在這以前要鞏固數學基礎,我在看網易公開課上的《線性代數》和《微積分》的公開課。
好在,這些「入門知識」,平日工做倒也夠用,而數學、計算機基礎的鞏固和學習,是一生的事,不用太急。
不要哀嘆過去的荒蕪,惟一的方法是立足於如今的狀態,選取最優的進步路線,勤勉地躬行之。
學過一點點經濟學基礎的人,不會爲「沉沒成本」沮喪。
數學太美,邏輯太美,算法太美。
我卻近來才發現。讀書的時候全沒免得。
不免得也不要緊,老老實實聽話就好,如今想一想,是「獨立思考」得過於早了。
這是個人人生歷程,沒什麼可後悔,寫這只是想規諫還在念書的朋友。
別急着自行其是,之後的人生裏,有的是機會讓你自行決策,還在念書的時候,請聽話。
聽老師的話,好好學習,每天向上。