《啊哈!算法》是一本充滿智慧和趣味的算法入門書。沒(méi)有枯燥的描述,沒(méi)有難懂的公式,一切以實(shí)際應(yīng)用為出發(fā)點(diǎn),通過(guò)幽默的語(yǔ)言配以可愛(ài)的插圖來(lái)講解算法。你更像是在閱讀一個(gè)個(gè)輕松的小故事或是在玩一把趣味解謎游戲,在輕松愉悅中便掌握算法精髓,感受算法之美。
《啊哈!算法》中涉及的數(shù)據(jù)結(jié)構(gòu)有棧、隊(duì)列、鏈表、樹(shù)、并查集、堆和圖等;涉及的算法有排序、枚舉、深度和廣度優(yōu)先搜索、圖的遍歷,當(dāng)然還有圖論中不可以缺少的四種最短路徑算法、兩種最小生成樹(shù)算法、割點(diǎn)與割邊算法、二分圖的最大匹配算法等。
啊哈!去中科院玩單片機(jī)
呦吼!在微軟亞洲研究院寫爬蟲
噠噠!寫一本開(kāi)開(kāi)心心的算法書
你一定能看懂的算法書!
作為本書的策劃編輯,我很榮幸。
《啊哈!算法》是我讀過(guò)的有趣且是我唯一能看懂的一本算法書。
我最初是因?yàn)榘」趯懙牧硗庖槐緯栋」!C》而認(rèn)識(shí)啊哈磊的。啊哈磊還有個(gè)網(wǎng)站,也叫啊哈磊,這個(gè)啊哈磊網(wǎng)站中有個(gè)論壇,叫啊哈論壇。論壇建立短短一年半時(shí)間,就聚集了15000多個(gè)啊哈小伙伴,都是萌物。我對(duì)他的寫作風(fēng)格很欣賞,那是一種因熱愛(ài)和探究而產(chǎn)生的純粹的快樂(lè),因此,當(dāng)啊哈磊率領(lǐng)著他的一大波萌物開(kāi)開(kāi)心心地攻城略地,浩浩蕩蕩地兵臨城下,跟我說(shuō)他想寫一本通俗易懂的算法書,不知是否能出版時(shí),我的回答是:“必須出版!”
這本書出版意向的達(dá)成就是這么簡(jiǎn)單。
但創(chuàng)作的過(guò)程一點(diǎn)不輕松。因?yàn)槿魏我槐灸玫贸鍪值臅膭?chuàng)作都是作者大量時(shí)間和精力付出的結(jié)果。是毅力的累積。
幾個(gè)月之后,我拿到了這本書的初稿。我高高興興地開(kāi)始讀。這部分寫得通俗易懂,我看得津津有味。但讀了一些之后,我發(fā)現(xiàn)我高興不起來(lái)了,我遇到了困難,有些篇章寫得太簡(jiǎn)略了,只是把算法的基本思路說(shuō)了一下,然后就直接給出了以該算法實(shí)現(xiàn)的某個(gè)示例的完整代碼。
這樣不行,看不懂啊。原理很簡(jiǎn)單,但實(shí)現(xiàn)起來(lái)時(shí),看代碼就感覺(jué)對(duì)應(yīng)不起來(lái)了;蛟S比我聰明的人能看懂,但我希望像我這種在算法方面毫無(wú)造詣的普通選手讀起來(lái)也不吃力,于是我讓啊哈磊完善它。我是這么交代的——你得寫得讓我能看懂才行。這要求非常的簡(jiǎn)單,但也非常的暗黑。
經(jīng)過(guò)比我想象的要長(zhǎng)的時(shí)間,啊哈磊給了我第二版。
我繼續(xù)閱讀,很多之前看不懂的地方現(xiàn)在能看懂了,或者至少我認(rèn)為我看懂了(請(qǐng)?jiān)试S我使用這種讓人生氣的措辭),但還有少部分欠點(diǎn)勁兒。啊哈磊向我投來(lái)困惑又略帶鄙視的目光,我用堅(jiān)定又癡癡呆呆的目光把他的目光給頂了回去。
于是啊哈磊繼續(xù)埋頭苦干。
終于,我完全可以看懂的版本誕生了。
對(duì)于一本技術(shù)書,一個(gè)編輯可能犯下的最有價(jià)值的“錯(cuò)誤”就是試圖去完全讀懂它。
在最后,我還要特別強(qiáng)調(diào)一點(diǎn),這本書不僅寫得通俗易懂,而且還在一個(gè)非常重要的方方面超越了其他技術(shù)書,那就是這本書中還配了可愛(ài)的漫畫,萌萌的畫風(fēng),生動(dòng)的場(chǎng)景,與文字渾然一體。
我想寫一本通俗易懂的算法書很久了,因?yàn)閷?duì)于多數(shù)人而言,“算法”給他的第一印象就是很難懂,其實(shí)我也是這樣。還記得我第一次學(xué)習(xí)圖論的“割點(diǎn)割邊”算法時(shí),看過(guò)不下于四五本書,其中不乏一些算法經(jīng)典書籍,還百度了一堆材料,才勉強(qiáng)將其看懂并實(shí)現(xiàn)成代碼。其實(shí)這個(gè)算法并不難,核心代碼不超過(guò)20行,但是很多算法書都是草草敘述,不同的書籍給出的參考代碼也是五花八門,有的甚至都不稀罕給你代碼,這大大增加了學(xué)習(xí)的難度。我是花了整整一個(gè)晚上才搞定的,當(dāng)然這其中不排除智商因素。第二印象就是算法是枯燥無(wú)趣的,并且好像沒(méi)什么作用。其實(shí)在我們的日常生活之中到處都可見(jiàn)到算法的影子,只不過(guò)它通常隱匿在事物的背后,不太容易被發(fā)現(xiàn)。但是它每天都在默默地為我們服務(wù)著。在本書中我將帶你一步步揭開(kāi)算法的奧秘,帶它走近你的身邊。
由于算法的內(nèi)容確實(shí)是太多了,要想全部寫清楚恐怕幾本書都不夠,本書將介紹一些最常用的算法。此外算法的實(shí)現(xiàn)通常需要依附一些數(shù)據(jù)結(jié)構(gòu),因此在必要的時(shí)候?qū)τ谛枰玫降臄?shù)據(jù)結(jié)構(gòu)我也會(huì)進(jìn)行講解。本書中涉及的數(shù)據(jù)結(jié)構(gòu)有棧、隊(duì)列、樹(shù)、并查集、堆和圖等;算法有各種排序、枚舉、深度和廣度優(yōu)先搜索、圖上的遍歷,當(dāng)然還有圖論中不可以缺少的四種最短路徑算法、兩種最小生成樹(shù)算法、割點(diǎn)與割邊算法、二分圖的最大匹配算法等。
盡管我不敢保證我寫的算法你一定可以看懂(但憑著一股強(qiáng)大的自信,我認(rèn)為初中以上文化程度的應(yīng)該沒(méi)問(wèn)題^_^),但我會(huì)以一個(gè)故事或者一個(gè)你在生活中可能遇到的問(wèn)題開(kāi)始對(duì)一個(gè)算法進(jìn)行講解,并盡量用通俗易懂的語(yǔ)言配合有趣的插圖讓你在閱讀本書的時(shí)候更像是在品讀一篇篇輕松的短篇小說(shuō)或是在玩一把趣味解謎游戲,在輕松愉悅中掌握算法精髓,感受算法之美。
致 謝
本書能得以面世,首先要感謝圖靈的陳冰先生。感謝你主動(dòng)聯(lián)系我,給予我信心去完成本書的全部,并且提出了很多寶貴的建議。更加令我吃驚的是你竟然能讀懂本書的全部算法(包括每一行代碼),還發(fā)現(xiàn)了很多隱藏得很深的錯(cuò)誤,真是一位非常棒的圖書出版人。
在書稿創(chuàng)作的過(guò)程中,有幸和很多優(yōu)秀的學(xué)生共同學(xué)習(xí)和探討,是他們?yōu)楸緯膭?chuàng)作提供了靈感,感謝他們的傾聽(tīng)、交流和建議。他們是武漢二中的呂凱風(fēng)同學(xué)、武漢外國(guó)語(yǔ)學(xué)校的李嘉浩、熊子健、陳雨禾、郭明達(dá)和李丁等同學(xué)。
本書之所以變得這么有趣,還必須要感謝我的美女插畫師鄭佳茜,你靈感涌現(xiàn)的插圖功不可沒(méi)。
感謝我的好友張知嚴(yán),無(wú)私地幫助我搭建了“添柴”編程在線學(xué)習(xí)系統(tǒng)(tianchai.org),為本書讀者提供了更好的學(xué)習(xí)交流平臺(tái)。
感謝我的學(xué)生胡夢(mèng)清,感謝你排除萬(wàn)難來(lái)參加你人生中的最后一場(chǎng)NOIP競(jìng)賽。是你用行動(dòng)、青春路上追求夢(mèng)想的精神,告訴我們18歲就應(yīng)該可愛(ài)、執(zhí)著、不畏懼,敢于朝著夢(mèng)想前行。
特別感謝我的未婚妻Snowin,是你放棄了近一年來(lái)所有的周末和節(jié)假日,陪我在書桌旁、咖啡廳里、旅途中……共同完成了本書的每一個(gè)字、每一幅圖、每一段代碼。
最后要感謝我的父母,你們把我拉扯大太不容易了,我愛(ài)你們!
啊哈磊
2014年5月6日
紀(jì)磊,網(wǎng)名啊哈磊。
曾在中科院玩過(guò)單片機(jī)。武漢大學(xué)歷史上第一位以本科生身份加入MSRA(微軟亞洲研究院)的小伙伴,在機(jī)器學(xué)習(xí)組從事搜索引擎方面的研究。
發(fā)表國(guó)際會(huì)議論文一篇(IEEE)。
全國(guó)青少年信息學(xué)奧林匹克金牌教練。
超萌超簡(jiǎn)潔的C語(yǔ)言編譯器——“啊哈C編譯器”作者。
2013年,我的第一部著作,有趣的編程科普書《啊哈C!》出版。
非常喜歡小朋友,每天都過(guò)得都非常開(kāi)心。
至于為什么叫“啊哈磊”,因?yàn)槲矣X(jué)得這是一個(gè)很喜慶的名字。
第1章 一大波數(shù)正在靠近——排序 1
第1節(jié) 最快最簡(jiǎn)單的排序——桶排序 2
第2節(jié) 鄰居好說(shuō)話——冒泡排序 7
第3節(jié) 最常用的排序——快速排序 12
第4節(jié) 小哼買書 20
第2章 棧、隊(duì)列、鏈表 25
第1節(jié) 解密QQ號(hào)——隊(duì)列 26
第2節(jié) 解密回文——!32
第3節(jié) 紙牌游戲——小貓釣魚 35
第4節(jié) 鏈表 44
第5節(jié) 模擬鏈表 54
第3章 枚舉!很暴力 57
第1節(jié) 坑爹的奧數(shù) 58
第2節(jié) 炸彈人 61
第3節(jié) 火柴棍等式 67
第4節(jié) 數(shù)的全排列 70
第4章 萬(wàn)能的搜索 72
第1節(jié) 不撞南墻不回頭——深度優(yōu)先搜索 73
第2節(jié) 解救小哈 81
第3節(jié) 層層遞進(jìn)——廣度優(yōu)先搜索 88
第4節(jié) 再解炸彈人 95
第5節(jié) 寶島探險(xiǎn) 106
第6節(jié) 水管工游戲 117
第5章 圖的遍歷 128
第1節(jié) 深度和廣度優(yōu)先究竟是指啥 129
第2節(jié) 城市地圖——圖的深度優(yōu)先遍歷 136
第3節(jié) 最少轉(zhuǎn)機(jī)——圖的廣度優(yōu)先遍歷 142
第6章 最短路徑 147
第1節(jié) 只有五行的算法——Floyd-Warshall 148
第2節(jié) Dijkstra算法——通過(guò)邊實(shí)現(xiàn)松弛 155
第3節(jié) Bellman-Ford——解決負(fù)權(quán)邊 163
第4節(jié) Bellman-Ford的隊(duì)列優(yōu)化 171
第5節(jié) 最短路徑算法對(duì)比分析 177
第7章 神奇的樹(shù) 178
第1節(jié) 開(kāi)啟“樹(shù)”之旅 179
第2節(jié) 二叉樹(shù) 183
第3節(jié) 堆——神奇的優(yōu)先隊(duì)列 185
第4節(jié) 擒賊先擒王——并查集 200
第8章 更多精彩算法 211
第1節(jié) 鏢局運(yùn)鏢——圖的最小生成樹(shù) 212
第2節(jié) 再談最小生成樹(shù) 219
第3節(jié) 重要城市——圖的割點(diǎn) 229
第4節(jié) 關(guān)鍵道路——圖的割邊 234
第5節(jié) 我要做月老——二分圖最大匹配 237
第9章 還能更好嗎——微軟亞洲研究院面試 243
第1節(jié) 最快最簡(jiǎn)單的排序——桶排序
在我們生活的這個(gè)世界中到處都是被排序過(guò)的東東。站隊(duì)的時(shí)候會(huì)按照身高排序,考試的名次需要按照分?jǐn)?shù)排序,網(wǎng)上購(gòu)物的時(shí)候會(huì)按照價(jià)格排序,電子郵箱中的郵件按照時(shí)間排序……總之很多東東都需要排序,可以說(shuō)排序是無(wú)處不在,F(xiàn)在我們舉個(gè)具體的例子來(lái)介紹一下排序算法。
首先出場(chǎng)的是我們的主人公小哼,上面這個(gè)可愛(ài)的娃就是啦。期末考試完了老師要將同學(xué)們的分?jǐn)?shù)按照從高到低排序。小哼的班上只有5個(gè)同學(xué),這5個(gè)同學(xué)分別考了5分、3分、5分、2分和8分,哎,考得真是慘不忍睹(滿分是10分)。接下來(lái)將分?jǐn)?shù)進(jìn)行從大到小排序,排序后是8 5 5 3 2。你有沒(méi)有什么好方法編寫一段程序,讓計(jì)算機(jī)隨機(jī)讀入5個(gè)數(shù)然后將這5個(gè)數(shù)從大到小輸出?請(qǐng)先想一想,至少想15分鐘再往下看吧(*^__^*)。
我們這里只需借助一個(gè)一維數(shù)組就可以解決這個(gè)問(wèn)題。請(qǐng)確定你真的仔細(xì)想過(guò)再往下看哦。
首先我們需要申請(qǐng)一個(gè)大小為11的數(shù)組int a[11]。OK,現(xiàn)在你已經(jīng)有了11個(gè)變量,編號(hào)從a[0]~a[10]。剛開(kāi)始的時(shí)候,我們將a[0]~a[10]都初始化為0,表示這些分?jǐn)?shù)還都沒(méi)有人得過(guò)。例如a[0]等于0就表示目前還沒(méi)有人得過(guò)0分,同理a[1]等于0就表示目前還沒(méi)有人得過(guò)1分……a[10]等于0就表示目前還沒(méi)有人得過(guò)10分。
下面開(kāi)始處理每一個(gè)人的分?jǐn)?shù),第一個(gè)人的分?jǐn)?shù)是5分,我們就將相對(duì)應(yīng)的a[5]的值在原來(lái)的基礎(chǔ)增加1,即將a[5]的值從0改為1,表示5分出現(xiàn)過(guò)了一次。
第二個(gè)人的分?jǐn)?shù)是3分,我們就把相對(duì)應(yīng)的a[3]的值在原來(lái)的基礎(chǔ)上增加1,即將a[3]的值從0改為1,表示3分出現(xiàn)過(guò)了一次。
注意啦!第三個(gè)人的分?jǐn)?shù)也是5分,所以a[5]的值需要在此基礎(chǔ)上再增加1,即將a[5]的值從1改為2,表示5分出現(xiàn)過(guò)了兩次。
按照剛才的方法處理第四個(gè)和第五個(gè)人的分?jǐn)?shù)。最終結(jié)果就是下面這個(gè)圖啦。
你發(fā)現(xiàn)沒(méi)有,a[0]~a[10]中的數(shù)值其實(shí)就是0分到10分每個(gè)分?jǐn)?shù)出現(xiàn)的次數(shù)。接下來(lái),我們只需要將出現(xiàn)過(guò)的分?jǐn)?shù)打印出來(lái)就可以了,出現(xiàn)幾次就打印幾次,具體如下。
a[0]為0,表示“0”沒(méi)有出現(xiàn)過(guò),不打印。
a[1]為0,表示“1”沒(méi)有出現(xiàn)過(guò),不打印。
a[2]為1,表示“2”出現(xiàn)過(guò)1次,打印2。
a[3]為1,表示“3”出現(xiàn)過(guò)1次,打印3。
a[4]為0,表示“4”沒(méi)有出現(xiàn)過(guò),不打印。
a[5]為2,表示“5”出現(xiàn)過(guò)2次,打印5 5。
a[6]為0,表示“6”沒(méi)有出現(xiàn)過(guò),不打印。
a[7]為0,表示“7”沒(méi)有出現(xiàn)過(guò),不打印。
a[8]為1,表示“8”出現(xiàn)過(guò)1次,打印8。
a[9]為0,表示“9”沒(méi)有出現(xiàn)過(guò),不打印。
a[10]為0,表示“10”沒(méi)有出現(xiàn)過(guò),不打印。
最終屏幕輸出“2 3 5 5 8”,完整的代碼如下。
#include
int main()
{
int a[11],i,j,t;
for(i=0;i<=10;i++)
a[i]=0; //初始化為0
for(i=1;i<=5;i++) //循環(huán)讀入5個(gè)數(shù)
{
scanf("%d",&t); //把每一個(gè)數(shù)讀到變量t中
a[t]++; //進(jìn)行計(jì)數(shù)
}
for(i=0;i<=10;i++) //依次判斷a[0]~a[10]
for(j=1;j<=a[i];j++) //出現(xiàn)了幾次就打印幾次
printf("%d ",i);
getchar();getchar();
//這里的getchar();用來(lái)暫停程序,以便查看程序輸出的內(nèi)容
//也可以用system("pause");等來(lái)代替
return 0;
}
輸入數(shù)據(jù)為:
5 3 5 2 8
仔細(xì)觀察的同學(xué)會(huì)發(fā)現(xiàn),剛才實(shí)現(xiàn)的是從小到大排序。但是我們要求是從大到小排序,這該怎么辦呢?還是先自己想一想再往下看哦。
其實(shí)很簡(jiǎn)單。只需要將for(i=0;i<=10;i++)改為for(i=10;i>=0;i--)就OK啦,快去試一試吧。
這種排序方法我們暫且叫它“桶排序”。因?yàn)槠鋵?shí)真正的桶排序要比這個(gè)復(fù)雜一些,以后再詳細(xì)討論,目前此算法已經(jīng)能夠滿足我們的需求了。
這個(gè)算法就好比有11個(gè)桶,編號(hào)從0~10。每出現(xiàn)一個(gè)數(shù),就在對(duì)應(yīng)編號(hào)的桶中放一個(gè)小旗子,最后只要數(shù)數(shù)每個(gè)桶中有幾個(gè)小旗子就OK了。例如2號(hào)桶中有1個(gè)小旗子,表示2出現(xiàn)了一次;3號(hào)桶中有1個(gè)小旗子,表示3出現(xiàn)了一次;5號(hào)桶中有2個(gè)小旗子,表示5出現(xiàn)了兩次;8號(hào)桶中有1個(gè)小旗子,表示8出現(xiàn)了一次。
現(xiàn)在你可以嘗試一下輸入n個(gè)0~1000之間的整數(shù),將它們從大到小排序。提醒一下,如果需要對(duì)數(shù)據(jù)范圍在0~1000的整數(shù)進(jìn)行排序,我們需要1001個(gè)桶,來(lái)表示0~1000之間每一個(gè)數(shù)出現(xiàn)的次數(shù),這一點(diǎn)一定要注意。另外,此處的每一個(gè)桶的作用其實(shí)就是“標(biāo)記”每個(gè)數(shù)出現(xiàn)的次數(shù),因此我喜歡將之前的數(shù)組a換個(gè)更貼切的名字book(book這個(gè)單詞有記錄、標(biāo)記的意思),代碼實(shí)現(xiàn)如下。
#include
int main()
{
int book[1001],i,j,t,n;
for(i=0;i<=1000;i++)
book[i]=0;
scanf("%d",&n);//輸入一個(gè)數(shù)n,表示接下來(lái)有n個(gè)數(shù)
for(i=1;i<=n;i++)//循環(huán)讀入n個(gè)數(shù),并進(jìn)行桶排序
{
scanf("%d",&t); //把每一個(gè)數(shù)讀到變量t中
book[t]++; //進(jìn)行計(jì)數(shù),對(duì)編號(hào)為t的桶放一個(gè)小旗子
}
for(i=1000;i>=0;i--) //依次判斷編號(hào)1000~0的桶
for(j=1;j<=book[i];j++) //出現(xiàn)了幾次就將桶的編號(hào)打印幾次
printf("%d ",i);
getchar();getchar();
return 0;
}
可以輸入以下數(shù)據(jù)進(jìn)行驗(yàn)證。
10
8 100 50 22 15 6 1 1000 999 0
運(yùn)行結(jié)果是:
1000 999 100 50 22 15 8 6 1 0
最后來(lái)說(shuō)下時(shí)間復(fù)雜度的問(wèn)題。代碼中第6行的循環(huán)一共循環(huán)了m次(m為桶的個(gè)數(shù)),第9行的代碼循環(huán)了n次(n為待排序數(shù)的個(gè)數(shù)),第14行和第15行一共循環(huán)了m+n次。所以整個(gè)排序算法一共執(zhí)行了m+n+m+n次。我們用大寫字母O來(lái)表示時(shí)間復(fù)雜度,因此該算法的時(shí)間復(fù)雜度是O(m+n+m+n)即O(2*(m+n))。我們?cè)谡f(shuō)時(shí)間復(fù)雜度的時(shí)候可以忽略較小的常數(shù),最終桶排序的時(shí)間復(fù)雜度為O(m+n)。還有一點(diǎn),在表示時(shí)間復(fù)雜度的時(shí)候,n和m通常用大寫字母即O(M+N)。
這是一個(gè)非?斓呐判蛩惴。桶排序從1956年就開(kāi)始被使用,該算法的基本思想是由E.J. Issac和R.C. Singleton提出來(lái)的。之前我說(shuō)過(guò),其實(shí)這并不是真正的桶排序算法,真正的桶排序算法要比這個(gè)更加復(fù)雜。但是考慮到此處是算法講解的第一篇,我想還是越簡(jiǎn)單易懂越好,真正的桶排序留在以后再聊吧。需要說(shuō)明一點(diǎn)的是:我們目前學(xué)習(xí)的簡(jiǎn)化版桶排序算法,其本質(zhì)上還不能算是一個(gè)真正意義上的排序算法。為什么呢?例如遇到下面這個(gè)例子就沒(méi)轍了。
現(xiàn)在分別有5個(gè)人的名字和分?jǐn)?shù):huhu 5分、haha 3分、xixi 5分、hengheng 2分和gaoshou 8分。請(qǐng)按照分?jǐn)?shù)從高到低,輸出他們的名字。即應(yīng)該輸出gaoshou、huhu、xixi、haha、hengheng。發(fā)現(xiàn)問(wèn)題了沒(méi)有?如果使用我們剛才簡(jiǎn)化版的桶排序算法僅僅是把分?jǐn)?shù)進(jìn)行了排序。最終輸出的也僅僅是分?jǐn)?shù),但沒(méi)有對(duì)人本身進(jìn)行排序。也就是說(shuō),我們現(xiàn)在并不知道排序后的分?jǐn)?shù)原本對(duì)應(yīng)著哪一個(gè)人!這該怎么辦呢?不要著急,請(qǐng)看下節(jié)——冒泡排序。
……