算法的樂(lè)趣(廣泛涵蓋常用算法結(jié)構(gòu)及應(yīng)用)_第1頁(yè)
算法的樂(lè)趣(廣泛涵蓋常用算法結(jié)構(gòu)及應(yīng)用)_第2頁(yè)
算法的樂(lè)趣(廣泛涵蓋常用算法結(jié)構(gòu)及應(yīng)用)_第3頁(yè)
算法的樂(lè)趣(廣泛涵蓋常用算法結(jié)構(gòu)及應(yīng)用)_第4頁(yè)
算法的樂(lè)趣(廣泛涵蓋常用算法結(jié)構(gòu)及應(yīng)用)_第5頁(yè)
已閱讀5頁(yè),還剩538頁(yè)未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

算法的樂(lè)趣廣泛涵蓋常用算法結(jié)構(gòu)及應(yīng)用目錄\h第1章程序員與算法\h1.1什么是算法\h1.2程序員必須要會(huì)算法嗎\h1.2.1一個(gè)隊(duì)列引發(fā)的慘案\h1.2.2我的第一個(gè)算法\h1.3算法的樂(lè)趣在哪里\h1.4算法與代碼\h1.5總結(jié)\h1.6參考資料\h第2章算法設(shè)計(jì)的基礎(chǔ)\h2.1程序的基本結(jié)構(gòu)\h2.1.1順序執(zhí)行\(zhòng)h2.1.2循環(huán)結(jié)構(gòu)\h2.1.3分支和跳轉(zhuǎn)結(jié)構(gòu)\h2.2算法實(shí)現(xiàn)與數(shù)據(jù)結(jié)構(gòu)\h2.2.1基本數(shù)據(jù)結(jié)構(gòu)在算法設(shè)計(jì)中的應(yīng)用\h2.2.2復(fù)雜數(shù)據(jù)結(jié)構(gòu)在算法設(shè)計(jì)中的應(yīng)用\h2.3數(shù)據(jù)結(jié)構(gòu)和數(shù)學(xué)模型與算法的關(guān)系\h2.4總結(jié)\h2.5參考資料\h第3章算法設(shè)計(jì)的常用思想\h3.1貪婪法\h3.1.1貪婪法的基本思想\h3.1.2貪婪法的例子:0-1背包問(wèn)題\h3.2分治法\h3.2.1分治法的基本思想\h3.2.2遞歸和分治,一對(duì)好朋友\h3.2.3分治法的例子:大整數(shù)Karatsuba乘法算法\h3.3動(dòng)態(tài)規(guī)劃\h3.3.1動(dòng)態(tài)規(guī)劃的基本思想\h3.3.2動(dòng)態(tài)規(guī)劃法的例子:字符串的編輯距離\h3.4解空間的窮舉搜索\h3.4.1解空間的定義\h3.4.2窮舉解空間的策略\h3.4.3窮舉搜索的例子:Google方程式\h3.5總結(jié)\h3.6參考資料\h第4章阿拉伯?dāng)?shù)字與中文數(shù)字\h4.1中文數(shù)字的特點(diǎn)\h4.1.1中文數(shù)字的權(quán)位和小節(jié)\h4.1.2中文數(shù)字的零\h4.2阿拉伯?dāng)?shù)字轉(zhuǎn)中文數(shù)字\h4.2.1一個(gè)轉(zhuǎn)換示例\h4.2.2轉(zhuǎn)換算法設(shè)計(jì)\h4.2.3算法實(shí)現(xiàn)\h4.2.4中文大寫數(shù)字\h4.3中文數(shù)字轉(zhuǎn)阿拉伯?dāng)?shù)字\h4.3.1轉(zhuǎn)換的基本方法\h4.3.2算法實(shí)現(xiàn)\h4.4數(shù)字轉(zhuǎn)換的測(cè)試用例\h4.5總結(jié)\h4.6參考資料\h第5章三個(gè)水桶等分8升水的問(wèn)題\h5.1問(wèn)題與求解思路\h5.2建立數(shù)學(xué)模型\h5.2.1狀態(tài)的數(shù)學(xué)模型與狀態(tài)樹\h5.2.2倒水動(dòng)作的數(shù)學(xué)模型\h5.3搜索算法\h5.3.1狀態(tài)樹的遍歷\h5.3.2剪枝和重復(fù)狀態(tài)判斷\h5.4算法實(shí)現(xiàn)\h5.5總結(jié)\h5.6參考資料\h第6章妖怪與和尚過(guò)河問(wèn)題\h6.1問(wèn)題與求解思路\h6.2建立數(shù)學(xué)模型\h6.2.1狀態(tài)的數(shù)學(xué)模型與狀態(tài)樹\h6.2.2過(guò)河動(dòng)作的數(shù)學(xué)模型\h6.3搜索算法\h6.3.1狀態(tài)樹的遍歷\h6.3.2剪枝和重復(fù)狀態(tài)判斷\h6.4算法實(shí)現(xiàn)\h6.5總結(jié)\h6.6參考資料\h第7章穩(wěn)定匹配與舞伴問(wèn)題\h7.1穩(wěn)定匹配問(wèn)題\h7.1.1什么是穩(wěn)定匹配\h7.1.2Gale-Shapley算法原理\h7.2Gale-Shapley算法的應(yīng)用實(shí)例\h7.2.1算法實(shí)現(xiàn)\h7.2.2改進(jìn)優(yōu)化:空間換時(shí)間\h7.3有多少穩(wěn)定匹配\h7.3.1窮舉所有的完美匹配\h7.3.2不穩(wěn)定因素的判斷算法\h7.3.3窮舉的結(jié)果\h7.4二部圖與二分匹配\h7.4.1最大匹配與匈牙利算法\h7.4.2帶權(quán)匹配與Kuhn-Munkres算法\h7.5總結(jié)\h7.6參考資料\h第8章愛(ài)因斯坦的思考題\h8.1問(wèn)題的答案\h8.2分析問(wèn)題的數(shù)學(xué)模型\h8.2.1基本模型定義\h8.2.2線索模型定義\h8.3算法設(shè)計(jì)\h8.3.1窮舉所有的組合結(jié)果\h8.3.2利用線索判定結(jié)果的正確性\h8.4總結(jié)\h8.5參考資料\h第9章項(xiàng)目管理與圖的拓?fù)渑判騖h9.1AOV網(wǎng)和AOE網(wǎng)\h9.2拓?fù)渑判騖h9.2.1拓?fù)渑判虻幕具^(guò)程\h9.2.2按照活動(dòng)開始時(shí)間排序\h9.3關(guān)鍵路徑算法\h9.3.1什么是關(guān)鍵路徑\h9.3.2計(jì)算關(guān)鍵路徑的算法\h9.4總結(jié)\h9.5參考資料\h第10章RLE壓縮算法與PCX圖像文件格式\h10.1RLE壓縮算法\h10.1.1連續(xù)重復(fù)數(shù)據(jù)的處理\h10.1.2連續(xù)非重復(fù)數(shù)據(jù)的處理\h10.1.3算法實(shí)現(xiàn)\h10.2RLE與PCX圖像文件格式\h10.2.1PCX圖像文件格式\h10.2.2PCX_RLE算法\h10.2.3256色PCX文件的解碼和顯示\h10.3總結(jié)\h10.4參考資料\h第11章算法與歷法\h11.1格里歷(公歷)生成算法\h11.1.1格里歷的歷法規(guī)則\h11.1.2今天星期幾\h11.1.3生成日歷的算法\h11.1.4日歷變更那點(diǎn)事兒\h11.2二十四節(jié)氣的天文學(xué)計(jì)算\h11.2.1二十四節(jié)氣的起源\h11.2.2二十四節(jié)氣的天文學(xué)定義\h11.2.3VSOP-82/87行星理論\h11.2.4誤差修正——章動(dòng)\h11.2.5誤差修正——光行差\h11.2.6用牛頓迭代法計(jì)算二十四節(jié)氣\h11.3農(nóng)歷朔日(新月)的天文學(xué)計(jì)算\h11.3.1日月合朔的天文學(xué)定義\h11.3.2ELP-2000/82月球理論\h11.3.3誤差修正——地球軌道離心率修正\h11.3.4誤差修正——黃經(jīng)攝動(dòng)\h11.3.5月球地心視黃經(jīng)和最后的修正——地球章動(dòng)\h11.3.6用牛頓迭代法計(jì)算日月合朔\h11.4農(nóng)歷的生成算法\h11.4.1中國(guó)農(nóng)歷的起源與歷法規(guī)則\h11.4.2中國(guó)農(nóng)歷的推算\h11.4.3一個(gè)簡(jiǎn)單的“年歷”\h11.5總結(jié)\h11.6參考資料\h第12章實(shí)驗(yàn)數(shù)據(jù)與曲線擬合\h12.1曲線擬合\h12.1.1曲線擬合的定義\h12.1.2簡(jiǎn)單線性數(shù)據(jù)擬合的例子\h12.2最小二乘法曲線擬合\h12.2.1最小二乘法原理\h12.2.2高斯消元法求解方程組\h12.2.3最小二乘法解決“速度與加速度”實(shí)驗(yàn)\h12.3三次樣條曲線擬合\h12.3.1插值函數(shù)\h12.3.2樣條函數(shù)的定義\h12.3.3邊界條件\h12.3.4推導(dǎo)三次樣條函數(shù)\h12.3.5追趕法求解方程組\h12.3.6三次樣條曲線擬合算法實(shí)現(xiàn)\h12.3.7三次樣條曲線擬合的效果\h12.4總結(jié)\h12.5參考資料\h第13章非線性方程與牛頓迭代法\h13.1非線性方程求解的常用方法\h13.1.1公式法\h13.1.2二分逼近法\h13.2牛頓迭代法的數(shù)學(xué)原理\h13.3用牛頓迭代法求解非線性方程的實(shí)例\h13.3.1導(dǎo)函數(shù)的求解與近似公式\h13.3.2算法實(shí)現(xiàn)\h13.4參考資料\h第14章計(jì)算幾何與計(jì)算機(jī)圖形學(xué)\h14.1計(jì)算幾何的基本算法\h14.1.1點(diǎn)與矩形的關(guān)系\h14.1.2點(diǎn)與圓的關(guān)系\h14.1.3矢量的基礎(chǔ)知識(shí)\h14.1.4點(diǎn)與直線的關(guān)系\h14.1.5直線與直線的關(guān)系\h14.1.6點(diǎn)與多邊形的關(guān)系\h14.2直線生成算法\h14.2.1什么是光柵圖形掃描轉(zhuǎn)換\h14.2.2數(shù)值微分法\h14.2.3Bresenham算法\h14.2.4對(duì)稱直線生成算法\h14.2.5兩步算法\h14.2.6其他直線生成算法\h14.3圓生成算法\h14.3.1圓的八分對(duì)稱性\h14.3.2中點(diǎn)畫圓法\h14.3.3改進(jìn)的中點(diǎn)畫圓法——Bresenham算法\h14.3.4正負(fù)判定畫圓法\h14.4橢圓生成算法\h14.4.1中點(diǎn)畫橢圓法\h14.4.2Bresenham橢圓算法\h14.5多邊形區(qū)域填充算法\h14.5.1種子填充算法\h14.5.2掃描線填充算法\h14.5.3改進(jìn)的掃描線填充算法\h14.5.4邊界標(biāo)志填充算法\h14.6總結(jié)\h14.7參考資料\h第15章音頻頻譜和均衡器與傅里葉變換算法\h15.1實(shí)時(shí)頻譜顯示的原理\h15.2離散傅里葉變換\h15.2.1什么是傅里葉變換\h15.2.2傅里葉變換原理\h15.2.3快速傅里葉變換算法的實(shí)現(xiàn)\h15.3傅里葉變換與音頻播放的實(shí)時(shí)頻譜顯示\h15.3.1頻域數(shù)值的特點(diǎn)分析\h15.3.2從音頻數(shù)據(jù)到功率頻譜\h15.3.3音頻播放時(shí)實(shí)時(shí)頻譜顯示的例子\h15.4破解電話號(hào)碼的小把戲\h15.4.1撥號(hào)音的頻譜分析\h15.4.2根據(jù)頻譜數(shù)據(jù)反推電話號(hào)碼\h15.5離散傅里葉逆變換\h15.5.1快速傅里葉逆變換的推導(dǎo)\h15.5.2快速傅里葉逆變換的算法實(shí)現(xiàn)\h15.6利用傅里葉變換實(shí)現(xiàn)頻域均衡器\h15.6.1頻域均衡器的實(shí)現(xiàn)原理\h15.6.2頻域信號(hào)的增益與衰減\h15.6.3均衡器的實(shí)現(xiàn)——仿Foobar的18段均衡器\h15.7總結(jié)\h15.8參考資料\h第16章全局最優(yōu)解與遺傳算法\h16.1遺傳算法的原理\h16.1.1遺傳算法的基本概念\h16.1.2遺傳算法的處理流程\h16.2遺傳算法求解0-1背包問(wèn)題\h16.2.1基因編碼和種群初始化\h16.2.2適應(yīng)度函數(shù)\h16.2.3選擇算子設(shè)計(jì)與輪盤賭算法\h16.2.4交叉算子設(shè)計(jì)\h16.2.5變異算子設(shè)計(jì)\h16.2.6這就是遺傳算法\h16.3總結(jié)\h16.4參考資料\h第17章計(jì)算器程序與大整數(shù)計(jì)算\h17.1哦,溢出了,出洋相的計(jì)算器程序\h17.2大整數(shù)計(jì)算的原理\h17.2.1大整數(shù)加法\h17.2.2大整數(shù)減法\h17.2.3大整數(shù)乘法\h17.2.4大整數(shù)除法與模\h17.2.5大整數(shù)乘方運(yùn)算\h17.3大整數(shù)類的使用\h17.3.1與Windows的計(jì)算器程序一決高下\h17.3.2最大公約數(shù)和最小公倍數(shù)\h17.3.3用擴(kuò)展歐幾里得算法求模的逆元\h17.4總結(jié)\h17.5參考資料\h第18章RSA算法——加密與簽名\h18.1RSA算法的開胃菜\h18.1.1將模冪運(yùn)算轉(zhuǎn)化為模乘運(yùn)算\h18.1.2模乘運(yùn)算與蒙哥馬利算法\h18.1.3模冪算法\h18.1.4素?cái)?shù)檢驗(yàn)與米勒-拉賓算法\h18.2RSA算法原理\h18.2.1RSA算法的數(shù)學(xué)理論\h18.2.2加密和解密算法\h18.2.3RSA算法的安全性\h18.3數(shù)據(jù)塊分組加密\h18.3.1字節(jié)流與大整數(shù)的轉(zhuǎn)換\h18.3.2PCKS與OAEP加密填充模式\h18.3.3數(shù)據(jù)加密算法實(shí)現(xiàn)\h18.3.4數(shù)據(jù)解密算法實(shí)現(xiàn)\h18.4RSA簽名與身份驗(yàn)證\h18.4.1RSASSA-PKCS與RSASSA-PSS簽名填充模式\h18.4.2簽名算法實(shí)現(xiàn)\h18.4.3驗(yàn)證簽名算法實(shí)現(xiàn)\h18.5總結(jié)\h18.6參考資料\h第19章數(shù)獨(dú)游戲\h19.1數(shù)獨(dú)游戲的規(guī)則與技巧\h19.1.1數(shù)獨(dú)游戲的規(guī)則\h19.1.2數(shù)獨(dú)游戲的常用技巧\h19.2計(jì)算機(jī)求解數(shù)獨(dú)問(wèn)題\h19.2.1建立問(wèn)題的數(shù)學(xué)模型\h19.2.2算法實(shí)現(xiàn)\h19.2.3與傳統(tǒng)窮舉方法的結(jié)果對(duì)比\h19.3關(guān)于數(shù)獨(dú)的趣味話題\h19.3.1數(shù)獨(dú)游戲有多少終盤\h19.3.2史上最難的數(shù)獨(dú)游戲\h19.4總結(jié)\h19.5參考資料\h第20章華容道游戲\h20.1華容道游戲介紹\h20.2自動(dòng)求解的算法原理\h20.2.1定義棋盤的局面\h20.2.2算法思路\h20.3自動(dòng)求解的算法實(shí)現(xiàn)\h20.3.1棋局狀態(tài)與Zobrist哈希算法\h20.3.2重復(fù)棋局和左右鏡像的處理\h20.3.3正確結(jié)果的判斷條件\h20.3.4武將棋子的移動(dòng)\h20.3.5棋局的搜索算法\h20.4總結(jié)\h20.5參考資料\h第21章A*尋徑算法\h21.1尋徑算法演示程序\h21.2Dijkstra算法\h21.2.1Dijkstra算法原理\h21.2.2Dijkstra算法實(shí)現(xiàn)\h21.2.3Dijkstra算法演示程序\h21.3帶啟發(fā)的搜索算法——A*算法\h21.3.1A*算法原理\h21.3.2常用的距離評(píng)估函數(shù)\h21.3.3A*算法實(shí)現(xiàn)\h21.4總結(jié)\h21.5參考資料\h第22章俄羅斯方塊游戲\h22.1俄羅斯方塊游戲規(guī)則\h22.2俄羅斯方塊游戲人工智能的算法原理\h22.2.1影響評(píng)價(jià)結(jié)果的因素\h22.2.2常用的俄羅斯方塊游戲人工智能算法\h22.2.3PierreDellacherie評(píng)估算法\h22.3PierreDellacherie算法實(shí)現(xiàn)\h22.3.1基本數(shù)學(xué)模型和數(shù)據(jù)結(jié)構(gòu)定義\h22.3.2算法實(shí)現(xiàn)\h22.4總結(jié)\h22.5參考資料\h第23章博弈樹與棋類游戲\h23.1棋類游戲的AI\h23.1.1博弈與博弈樹\h23.1.2極大極小值搜索算法\h23.1.3負(fù)極大極搜索算法\h23.1.4“α-β”剪枝算法\h23.1.5估值函數(shù)\h23.1.6置換表與哈希函數(shù)\h23.1.7開局庫(kù)與終局庫(kù)\h23.2井字棋——最簡(jiǎn)單的博弈游戲\h23.2.1棋盤與棋子的數(shù)學(xué)模型\h23.2.2估值函數(shù)與估值算法\h23.2.3如何產(chǎn)生走法(落子方法)\h23.3奧賽羅棋(黑白棋)\h23.3.1棋盤與棋子的數(shù)學(xué)模型\h23.3.2估值函數(shù)與估值算法\h23.3.3搜索算法實(shí)現(xiàn)\h23.3.4最終結(jié)果\h23.4五子棋\h23.4.1棋盤與棋子的數(shù)學(xué)模型\h23.4.2估值函數(shù)與估值算法\h23.4.3搜索算法實(shí)現(xiàn)\h23.4.4最終結(jié)果\h23.5總結(jié)\h23.6參考資料\h附錄A算法設(shè)計(jì)的常用技巧\hA.1數(shù)組下標(biāo)處理\hA.2一重循環(huán)實(shí)現(xiàn)兩重循環(huán)的功能\hA.3棋盤(迷宮)類算法方向遍歷\hA.4代碼的一致性處理技巧\hA.5鏈表和數(shù)組的配合使用\hA.6“以空間換時(shí)間”的常用技巧\hA.7利用表驅(qū)動(dòng)避免長(zhǎng)長(zhǎng)的switch-case\h附錄B一個(gè)棋類游戲的設(shè)計(jì)框架\hB.1代碼框架的整體結(jié)構(gòu)\hB.2代碼框架的使用方法注:原文檔電子版(非掃描),需要的請(qǐng)下載本文檔后留言謝謝。第1章程序員與算法本章的標(biāo)題既然是“程序員與算法”,就必然要涉及一個(gè)基本問(wèn)題,那就是“程序員是否必須會(huì)算法”。這是一個(gè)充滿爭(zhēng)議的問(wèn)題,雖然并不像“生存還是毀滅”之類的選擇那樣艱難而沉重,但也絕不是一個(gè)輕松的話題。朋友們?cè)谖业摹八惴ㄏ盗小辈┛蛯谏习l(fā)表的評(píng)論和回復(fù),并不都是我所期待的贊美和鼓勵(lì),也常常會(huì)有一些冷言冷語(yǔ)。比如,“窮舉也算是算法嗎”或者“請(qǐng)你說(shuō)明一下算法在XX系統(tǒng)中能起到什么作用”。有一次,一個(gè)網(wǎng)友通過(guò)郵件問(wèn)我:“你寫的都是小兒科的東西,幾十行代碼就能搞定,能不能整一點(diǎn)高深的算法?”我反問(wèn)他什么是他所理解的高深的算法,他答復(fù)說(shuō):“像遺傳算法、蟻群算法之類的。”于是我給了他一個(gè)遺傳算法求解0-1背包問(wèn)題的例子(參見第16章),并告訴他,這也就是幾十行代碼的算法,怎么理解成是高深的算法?他剛開始不承認(rèn)這是遺傳算法,直到我給了他DenisCormier公開在北卡羅來(lái)納州立大學(xué)服務(wù)器上的遺傳算法的源代碼后,他才相信他一直認(rèn)為深不可測(cè)的遺傳算法的原理原來(lái)是這么簡(jiǎn)單。還有一個(gè)網(wǎng)友直言我寫的“用三個(gè)水桶等分8升水”之類的問(wèn)題根本就稱不上算法,他認(rèn)為像“深藍(lán)”那樣的人工智能才算是算法。我告訴他計(jì)算機(jī)下棋的基本理論就是博弈樹,或者再加一個(gè)專家系統(tǒng)。但是他認(rèn)為博弈樹也是很高深的算法,于是我給了他一個(gè)井字棋游戲(參見第23章),并告訴他,這就是博弈樹搜索算法,非常智能,你絕對(duì)戰(zhàn)勝不了它(因?yàn)榫制逵螒蚝芎?jiǎn)單,這個(gè)算法會(huì)把所有的狀態(tài)都搜索完)。我相信他一定很震驚,因?yàn)檫@個(gè)算法也不超過(guò)100行代碼。對(duì)于上面提到的例子,我覺(jué)得主要原因在于大家對(duì)算法的理解有差異,很多人對(duì)算法的理解太片面,很多人覺(jué)得只有名字里包含“XX算法”之類的東西才是算法。而我認(rèn)為算法的本質(zhì)是解決問(wèn)題,只要是能解決問(wèn)題的代碼就是算法。在討論程序員與算法這個(gè)問(wèn)題之前,我們先探討一個(gè)最基本的問(wèn)題:什么是算法。1.1什么是算法《算法導(dǎo)論》一書將算法(algorithm)描述為定義良好的計(jì)算過(guò)程,它取一個(gè)或一組值作為輸入,并產(chǎn)生一個(gè)或一組值作為輸出。Knuth在《計(jì)算機(jī)程序設(shè)計(jì)藝術(shù)》一書中將算法描述為從一個(gè)步驟開始,按照既定的順序執(zhí)行完所有的步驟,最終結(jié)束(得到結(jié)果)的一個(gè)過(guò)程。Weiss在《數(shù)據(jù)結(jié)構(gòu)與算法分析》一書中將算法描述為一系列的計(jì)算步驟,將輸入數(shù)據(jù)轉(zhuǎn)換成輸出的結(jié)果。雖然沒(méi)有被普遍接受的“算法”的正式定義,但是各種著作中對(duì)算法的基本要素或基本特征的定義都是明確的,Knuth總結(jié)了算法的四大特征。確定性。算法的每個(gè)步驟都是明確的,對(duì)結(jié)果的預(yù)期也是確定的。有窮性。算法必須是由有限個(gè)步驟組成的過(guò)程,步驟的數(shù)量可能是幾個(gè),也可能是幾百萬(wàn)個(gè),但是必須有一個(gè)確定的結(jié)束條件。可行性。一般來(lái)說(shuō),我們期望算法最后得出的是正確的結(jié)果,這意味著算法中的每一個(gè)步驟都是可行的。只要有一個(gè)步驟不可行,算法就是失敗的,或者不能被稱為某種算法。輸入和輸出。算法總是要解決特定的問(wèn)題,問(wèn)題來(lái)源就是算法的輸入,期望的結(jié)果就是算法的輸出。沒(méi)有輸入的算法是沒(méi)有意義的,沒(méi)有輸出的算法是沒(méi)有用的。算法需要一定的數(shù)學(xué)基礎(chǔ),但是沒(méi)有任何文獻(xiàn)資料將算法限定于只解決數(shù)學(xué)問(wèn)題。有些人將貪婪法、分治法、動(dòng)態(tài)規(guī)劃法、線性規(guī)劃法、搜索和枚舉(包括窮盡枚舉)等方法理解為算法,其實(shí)這些只是設(shè)計(jì)算法常用的設(shè)計(jì)模式(Knuth稱之為設(shè)計(jì)范式)。同樣,計(jì)算機(jī)程序只是算法的一種存在形式,偽代碼、流程圖、各種符號(hào)和控制表格也是常見的算法展示形式。而順序執(zhí)行、并行執(zhí)行(包括分布式計(jì)算)、遞歸方法和迭代方法則是常用的算法實(shí)現(xiàn)方法。綜合以上分析和引述,本人將算法定義為:算法是為解決一個(gè)特定的問(wèn)題而精心設(shè)計(jì)的一套數(shù)學(xué)模型以及在這套數(shù)學(xué)模型上的一系列操作步驟,這些操作步驟將問(wèn)題描述的輸入數(shù)據(jù)逐步處理、轉(zhuǎn)換,并最后得到一個(gè)確定的結(jié)果。使用“精心設(shè)計(jì)”一詞,是因?yàn)槲覍⑺惴ǖ脑O(shè)計(jì)過(guò)程理解為人類頭腦中知識(shí)、經(jīng)驗(yàn)激烈碰撞的過(guò)程,將算法理解為最終“小宇宙爆發(fā)”一般得到的智力結(jié)果。1.2程序員必須要會(huì)算法嗎很多人可能是好萊塢大片看多了,以為計(jì)算機(jī)神通廣大,但事實(shí)不是這樣的。計(jì)算機(jī)其實(shí)是一種很傻的工具,傻到幾乎沒(méi)有智商(至少目前是這樣)。它可以連續(xù)幾年做同一件事情而毫無(wú)怨言,但是如果你不告訴它怎么做,它什么事情也不會(huì)做。最有創(chuàng)造性的活動(dòng)其實(shí)是由一種被稱為“程序員”的人做的,計(jì)算機(jī)做的只不過(guò)是人類不愿意做的體力活而已。比如圖像識(shí)別技術(shù),需要一個(gè)字節(jié)一個(gè)字節(jié)地處理數(shù)據(jù),提取數(shù)據(jù)的特征值,然后在海量的數(shù)據(jù)中比較、匹配這些特征值,直到累得兩眼昏花,人類才不會(huì)干這種傻事兒呢。計(jì)算機(jī)愿意做,但前提是你要告訴它怎么做。算法可以理解為這樣一種技術(shù),它將告訴計(jì)算機(jī)怎么做。有人將編程理解為搭積木,直接用別人開發(fā)好的組件、庫(kù),甚至是類或API就行了,并且美其名曰“不用重復(fù)發(fā)明輪子”。我認(rèn)為這其實(shí)就是所謂的系統(tǒng)集成,如果一個(gè)程序員每天的工作就是搭積木,那將是令人十分羨慕的事情,但是我知道,事實(shí)并不是這樣的。這樣搭積木式的編程計(jì)算機(jī)就可以做,沒(méi)有必要讓人來(lái)做,因?yàn)槿斯さ某杀靖哂谟?jì)算機(jī)。我遇到的更多的是在論壇里發(fā)帖求助的人,比如“求代碼,把一個(gè)固定格式的文本文件讀入內(nèi)存”,再比如“誰(shuí)能幫我把這個(gè)結(jié)構(gòu)數(shù)組排排序啊,書上的例子都是整數(shù)數(shù)組排序”。他們是如此地?zé)o助,如果不是論壇對(duì)回帖有積分獎(jiǎng)勵(lì)的話,恐怕不會(huì)有人理他們。我要說(shuō)的是,大多數(shù)程序員并不需要知道各種專業(yè)領(lǐng)域里的算法,但是你要會(huì)設(shè)計(jì)能夠解決你面臨問(wèn)題的算法。一些領(lǐng)域內(nèi)的經(jīng)典問(wèn)題,在前人的努力之下都有了高效的算法實(shí)現(xiàn),本書的很多章節(jié)都介紹了這樣的算法,比如穩(wěn)定匹配問(wèn)題、A*算法等。但是更多情況下,你所面臨的問(wèn)題并沒(méi)有現(xiàn)成的算法實(shí)現(xiàn),需要程序員具有創(chuàng)新的精神。算法設(shè)計(jì)需要具備很好的數(shù)學(xué)基礎(chǔ),但數(shù)學(xué)并不是唯一需要的知識(shí),計(jì)算機(jī)技術(shù)的一些基礎(chǔ)學(xué)科(比如數(shù)據(jù)結(jié)構(gòu))也是必需的知識(shí),有人說(shuō):程序=算法+數(shù)據(jù)結(jié)構(gòu),這個(gè)雖然不完全正確,但是提到了計(jì)算機(jī)程序最重要的兩點(diǎn),那就是算法和數(shù)據(jù)結(jié)構(gòu)。算法和數(shù)據(jù)結(jié)構(gòu)永遠(yuǎn)是緊密聯(lián)系在一起的,算法可以理解為解決問(wèn)題的思想,這是程序中最具有創(chuàng)造性的部分,也是一個(gè)程序有別于另一個(gè)程序的關(guān)鍵點(diǎn),而數(shù)據(jù)結(jié)構(gòu)就是這種思想的載體。再次重申一遍,我和大多數(shù)人一樣,并不是要求每個(gè)程序員都精通各種算法。大多數(shù)程序員可能在整個(gè)職業(yè)生涯中都不會(huì)遇到像ACM(AssociationforComputingMachinery)組織的國(guó)際大學(xué)生程序設(shè)計(jì)競(jìng)賽中的問(wèn)題,但是說(shuō)用不到數(shù)據(jù)結(jié)構(gòu)和算法則是不可想象的。說(shuō)數(shù)據(jù)結(jié)構(gòu)和算法沒(méi)用的人是因?yàn)樗麄冇貌坏?,用不到的原因是他們想不到,而想不到的原因是他們不?huì)。請(qǐng)息怒,我不是要打擊任何人,很多情況下確實(shí)是因?yàn)椴粫?huì),所以才用不到,下面就是一個(gè)典型的例子。1.2.1一個(gè)隊(duì)列引發(fā)的慘案我所在的團(tuán)隊(duì)負(fù)責(zé)一款光接入網(wǎng)產(chǎn)品的“EPON業(yè)務(wù)管理模塊”的開發(fā)和維護(hù)工作,這是電信級(jí)的網(wǎng)絡(luò)設(shè)備,因此對(duì)各方面性能的要求都非常高。有一天,一個(gè)負(fù)責(zé)集成測(cè)試的小伙兒跑過(guò)來(lái)對(duì)我說(shuō),今天的每日構(gòu)造版本出現(xiàn)異常,所有線卡(承載數(shù)據(jù)業(yè)務(wù)的板卡)的上線時(shí)間比昨天的版本慢了4分鐘左右。我很驚訝,對(duì)于一個(gè)電信級(jí)網(wǎng)絡(luò)設(shè)備來(lái)說(shuō),每次加電后的線卡上線時(shí)間就是業(yè)務(wù)恢復(fù)時(shí)間,業(yè)務(wù)恢復(fù)時(shí)間這么慢是不能接受的。于是我檢查了一下前一天的代碼入庫(kù)記錄,很快就找到了問(wèn)題所在。原來(lái)當(dāng)前版本的任務(wù)列表中有這樣一項(xiàng)功能,那就是記錄線卡的數(shù)據(jù)變更日志,需求的描述是在線卡上維護(hù)一個(gè)日志緩沖區(qū),每當(dāng)有用戶操作造成數(shù)據(jù)變更時(shí),就記錄一條變更信息,線卡上線時(shí)的批量數(shù)據(jù)同步也屬于操作數(shù)據(jù)變更,也要計(jì)入日志。因?yàn)槭乔度胧皆O(shè)備,線卡上日志緩沖區(qū)的大小受限制,最多只能存儲(chǔ)1000條記錄,當(dāng)記錄的日志超過(guò)1000條時(shí),新增的日志記錄將覆蓋舊的記錄,也就是說(shuō),這個(gè)日志緩沖區(qū)只保留最近寫入的1000條記錄。一個(gè)新來(lái)的小伙兒接受了這個(gè)任務(wù),并在前一天下班前將代碼簽入庫(kù)中(程序員要記住啊,一定不要在臨下班前簽入代碼)。他的實(shí)現(xiàn)方案大致是這樣的(注釋是我加上的):#defineSYNC_LOG_CNT1000

#defineSYNC_LOG_MEMOVER_CNT50

typedefstruct

{

INT32UlogCnt;

EPON_SYNC_LOG_DATAsyncLogs[SYNC_LOG_CNT];

}EPON_SYNC_LOG;

EPON_SYNC_LOGs_EponSyncLog;

voidEpon_Sync_Log_Add(EPON_SYNC_LOG_DATA*pLogData)

{

INT32Ui=0;

INT32UsyncLogCnt=0;

syncLogCnt=s_EponSyncLog.logCnt;

if(syncLogCnt>=SYNC_LOG_CNT)

{

/*緩沖區(qū)已滿,向前移動(dòng)950條記錄,為新紀(jì)錄騰出50條記錄的空間*/

memmove(s_EponSyncLog.syncLogs,

s_EponSyncLog.syncLogs+SYNC_LOG_MEMOVER_CNT,

(SYNC_LOG_CNT-SYNC_LOG_MEMOVER_CNT)*sizeof(EPON_SYNC_LOG_DATA));

/*清空新騰出來(lái)的空間*/

memset(s_EponSyncLog.syncLogs+(SYNC_LOG_CNT-SYNC_LOG_MEMOVER_CNT),

0,SYNC_LOG_MEMOVER_CNT*sizeof(EPON_SYNC_LOG_DATA));

/*寫入當(dāng)前一條日志*/

memmove(s_EponSyncLog.syncLogs+(SYNC_LOG_CNT-SYNC_LOG_MEMOVER_CNT),

pLogData,sizeof(EPON_SYNC_LOG_DATA));

s_EponSyncLog.logCnt=SYNC_LOG_CNT-SYNC_LOG_MEMOVER_CNT+1;

return;

}

/*如果緩沖區(qū)有空間,則直接寫入當(dāng)前一條記錄*/

memmove(s_EponSyncLog.syncLogs+syncLogCnt,

pLogData,sizeof(EPON_SYNC_LOG_DATA));

s_EponSyncLog.logCnt++;

}

這個(gè)方案使用一個(gè)長(zhǎng)度為1000條記錄的數(shù)組存儲(chǔ)日志,用一個(gè)計(jì)數(shù)器記錄當(dāng)前寫入的有效日志條數(shù),數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)中規(guī)中矩,但是當(dāng)緩沖區(qū)滿,需要覆蓋舊記錄時(shí)遇到了麻煩,因?yàn)槊看味家苿?dòng)數(shù)組中的前999條記錄,才能為新記錄騰出空間,這將使Epon_Sync_Log_Add()函數(shù)的性能急劇惡化??紤]到這一點(diǎn),小伙兒為他的方案設(shè)計(jì)了一個(gè)閾值,就是SYNC_LOG_MEMOVER_CNT常量定義的50。當(dāng)緩沖區(qū)滿的時(shí)候,就一次性向前移動(dòng)950條記錄,騰出50條記錄的空間,避免了每新增一條記錄就要移動(dòng)全部數(shù)據(jù)的情況??梢娺@個(gè)小伙兒還是動(dòng)了一番腦子的,在Epon_Sync_Log_Add()函數(shù)調(diào)用不是很頻繁的情況下,在功能和性能之間做了個(gè)折中,根據(jù)自測(cè)的情況,他覺(jué)得還可以,于是就在下班前匆匆簽入代碼,沒(méi)有來(lái)得及安排代碼走查和同行評(píng)審。但是他沒(méi)有考慮到線卡上線時(shí)需要批量同步數(shù)據(jù)的情況,在這種情況下,Epon_Sync_Log_Add()函數(shù)被調(diào)用的頻度仍然超出了這個(gè)閾值所能容忍的程度。通過(guò)對(duì)任務(wù)的性能進(jìn)行分析,我們發(fā)現(xiàn)大量的時(shí)間都花費(fèi)在Epon_Sync_Log_Add()函數(shù)中移動(dòng)記錄的操作上,即便是設(shè)計(jì)了閾值SYNC_LOG_MEMOVER_CNT,性能依然很差。其實(shí),類似這樣的固定長(zhǎng)度緩沖區(qū)的讀寫,環(huán)形隊(duì)列通常是最好的選擇。下面我們來(lái)看一下環(huán)形隊(duì)列的示意圖,如圖1-1所示。圖1-1環(huán)形隊(duì)列示意圖計(jì)算機(jī)內(nèi)存中沒(méi)有環(huán)形結(jié)構(gòu),因此環(huán)形隊(duì)列都是用線性表來(lái)實(shí)現(xiàn)的,當(dāng)數(shù)據(jù)指針到達(dá)線性表的尾部時(shí),就將它轉(zhuǎn)到0位置重新開始。實(shí)際編程的時(shí)候,也不需要每次都判斷數(shù)據(jù)指針是否到達(dá)線性表的尾部,通常用取模運(yùn)算對(duì)此做一致性處理。設(shè)模擬環(huán)形隊(duì)列的線性表的長(zhǎng)度是N,隊(duì)頭指針為head,隊(duì)尾指針為tail,則每增加一條記錄,就可用以下方法計(jì)算新的隊(duì)尾指針:tail=(tail+1)%N

對(duì)于本例的功能需求,當(dāng)tail+1等于head的時(shí)候,說(shuō)明隊(duì)列已滿,此時(shí)只需將head指針向前移動(dòng)一位,就可以在tail位置寫入新的記錄。使用環(huán)形隊(duì)列,可以避免移動(dòng)記錄操作,本節(jié)開始時(shí)提到的性能問(wèn)題就迎刃而解了。在這里,套用一句廣告詞:“沒(méi)有做不到,只有想不到?!笨纯矗覜](méi)說(shuō)錯(cuò)吧?1.2.2我的第一個(gè)算法我的第一份工作是為一個(gè)光柵圖像矢量化軟件編寫一個(gè)圖像預(yù)處理系統(tǒng),這套光柵圖像矢量化軟件能夠?qū)募堎|(zhì)工程圖紙掃描得到的位圖圖紙識(shí)別成能被各種CAD軟件處理的矢量化圖形文件。在預(yù)處理系統(tǒng)中有一個(gè)功能是對(duì)已經(jīng)二值化的光柵位圖(黑白兩色位圖)進(jìn)行污點(diǎn)消除。光柵位圖上的污點(diǎn)可能是原始圖紙上掃描前就存在的墨點(diǎn),也可能是掃描儀引入的噪點(diǎn),這些污點(diǎn)會(huì)對(duì)矢量化識(shí)別過(guò)程產(chǎn)生影響,會(huì)識(shí)別出錯(cuò)誤的圖形和符號(hào),因此需要預(yù)先消除這些污點(diǎn)。當(dāng)時(shí)我不知道有小波算法,也不知道還有各種圖像濾波算法,只是根據(jù)對(duì)問(wèn)題的認(rèn)識(shí),給出了我的解決方案。首先我觀察圖紙文件,像直線、圓和弧線這樣有意義的圖形都是最少有5個(gè)點(diǎn)相互連在一起構(gòu)成的,而污點(diǎn)一般都不會(huì)超過(guò)5個(gè)點(diǎn)連在一起(較大的污點(diǎn)都用其他的方法除掉了)。因此我給出了污點(diǎn)的定義:如果一個(gè)點(diǎn)周圍與之相連的點(diǎn)的總數(shù)小于5,則這幾個(gè)相連在一起的點(diǎn)就是一個(gè)污點(diǎn)。根據(jù)這個(gè)定義,我給出了我的算法:從位圖的第一個(gè)點(diǎn)開始搜索,如果這個(gè)點(diǎn)是1(1表示黑色,是圖紙上的點(diǎn);0表示白色,是圖紙背景顏色),就將相連點(diǎn)計(jì)數(shù)器加1,然后繼續(xù)向這個(gè)點(diǎn)相連的8個(gè)方向分別搜索,如果某個(gè)方向上的相鄰點(diǎn)是0就停止這個(gè)方向的搜索。如果搜索到的相連點(diǎn)超過(guò)4個(gè),說(shuō)明這個(gè)點(diǎn)是某個(gè)圖形上的點(diǎn),就退出這個(gè)點(diǎn)的搜索。如果搜索完成后得到的相連的點(diǎn)小于或等于4個(gè),就說(shuō)明這個(gè)點(diǎn)是一個(gè)污點(diǎn),需要將其顏色置為0(清除污點(diǎn))。算法實(shí)現(xiàn)首先定義搜索過(guò)程中存儲(chǔ)相連點(diǎn)信息的數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)定義如下:typedefstructtagRESULT

{

POINTpts[MAX_DIRTY_POINT];/*記錄搜索過(guò)的前5個(gè)點(diǎn)的位置*/

intcount;

}RESULT;

這個(gè)數(shù)據(jù)結(jié)構(gòu)有兩個(gè)屬性,count是搜索過(guò)程中發(fā)現(xiàn)的相連點(diǎn)的個(gè)數(shù),pts是記錄這些相連點(diǎn)位置的線性表。記錄這些點(diǎn)的位置是為了在搜索結(jié)束后,如果判定這些點(diǎn)是污點(diǎn),可以利用這些記錄的位置信息直接清除這些點(diǎn)的顏色。/*8個(gè)方向*/

POINTdir[]={{-1,0},{-1,-1},{0,-1},{1,-1},{1,0},{1,1},{0,1},{-1,1}};

voidSearchDirty(charbmp[MAX_BMP_WIDTH][MAX_BMP_HEIGHT]

intx,inty,RESULT*result)

{

for(inti=0;i<sizeof(dir)/sizeof(dir[0]);i++)

{

intnx=x+dir[i].x;

intny=y+dir[i].y;

if((nx>=0&&nx<MAX_BMP_WIDTH)

&&(ny>=0&&nx<MAX_BMP_HEIGHT)

&&(bmp[nx][ny]==1))

{

if(result->count<MAX_DIRTY_POINT)

{

/*記錄前MAX_DIRTY_POINT個(gè)點(diǎn)的位置*/

result->pts[result->count].x=nx;

result->pts[result->count].x=ny;

}

result->count++;

if(result->count>MAX_DIRTY_POINT)

break;

SearchDirty(bmp,nx,ny,result);

}

}

}

向8個(gè)方向搜索使用了預(yù)置的矢量數(shù)組dir,這是迷宮或棋盤類游戲搜索慣用的模式,本書介紹的算法會(huì)多次使用這種模式。SearchDirty()函數(shù)遞歸地調(diào)用自己,實(shí)現(xiàn)對(duì)8個(gè)方向的連通性搜索,最后的結(jié)果存在result中,如果count的個(gè)數(shù)大于4,說(shuō)明[x,y]位置的點(diǎn)是正常圖形上的點(diǎn),如果count的個(gè)數(shù)小于或等于4,則說(shuō)明[x,y]位置相鄰的這個(gè)點(diǎn)是一個(gè)污點(diǎn)。污點(diǎn)相鄰的點(diǎn)的位置都被記錄在pts中,將這些位置的位圖數(shù)據(jù)置0就消除了污點(diǎn)。算法沒(méi)有做任何優(yōu)化,不過(guò)好在圖紙上大部分都是白色背景,需要搜索的點(diǎn)并不多。打開測(cè)試圖紙一試,速度并不慢,效果也很好,幾個(gè)故意點(diǎn)上去做測(cè)試用的污點(diǎn)都沒(méi)有了,小的噪點(diǎn)也沒(méi)有了,圖紙一下就變白了。不過(guò)這段代碼最終并沒(méi)有成為那個(gè)軟件的一部分,學(xué)過(guò)機(jī)械制圖的同學(xué)可能看出來(lái)了,這個(gè)算法會(huì)將一些細(xì)小的虛線和點(diǎn)劃線一并干掉。這是一個(gè)微不足道的問(wèn)題,但卻是我第一次為解決(當(dāng)然,未遂)問(wèn)題而設(shè)計(jì)了一個(gè)算法,并最終用程序?qū)⑵鋵?shí)現(xiàn)。它讓我領(lǐng)悟到了一個(gè)道理,軟件被編寫出來(lái)就是為了解決問(wèn)題的,程序員的任務(wù)就是設(shè)計(jì)解決這些問(wèn)題的算法。成功固然高興,失敗也沒(méi)有什么代價(jià),可以隨時(shí)卷土重來(lái)。不要小看這些事情,不要以為只有各種專業(yè)領(lǐng)域的程序才會(huì)用到算法,每一個(gè)微小的設(shè)計(jì)都是算法創(chuàng)造性的體現(xiàn),即使失敗,也比放棄強(qiáng)。1.3算法的樂(lè)趣在哪里算法有很多種存在形式,編寫計(jì)算機(jī)程序只是其中一種,是程序員慣用的方式,本書要介紹的內(nèi)容就是如何以計(jì)算機(jī)程序的方式研究算法。1.2節(jié)介紹的兩個(gè)例子都是我親身經(jīng)歷過(guò)的事情,程序員在大部分時(shí)間里都是處理一些平凡而瑣碎的程序,但有時(shí)候也需要做一些創(chuàng)造性的工作。記住,程序員就是計(jì)算機(jī)的“上帝”,計(jì)算機(jī)能解決問(wèn)題是因?yàn)樗摹吧系邸备嬖V它怎么做。那么,當(dāng)問(wèn)題來(lái)臨的時(shí)候,“上帝”是到各種論壇上發(fā)帖子求代碼,還是自己解決問(wèn)題?如果要自己解決問(wèn)題,應(yīng)該如何解決問(wèn)題?為什么要自己解決問(wèn)題?先來(lái)回答第一個(gè)問(wèn)題——如何設(shè)計(jì)算法解決問(wèn)題?人類解決問(wèn)題的方式是當(dāng)遇到一個(gè)問(wèn)題時(shí),首先從大腦中搜索已有的知識(shí)和經(jīng)驗(yàn),尋找它們之間具有關(guān)聯(lián)的地方,將一個(gè)未知問(wèn)題做適當(dāng)?shù)霓D(zhuǎn)換,轉(zhuǎn)化成一個(gè)或多個(gè)已知問(wèn)題進(jìn)行求解,最后綜合起來(lái)得到原始問(wèn)題的解決方案。編寫計(jì)算機(jī)程序?qū)崿F(xiàn)算法,讓計(jì)算機(jī)幫我們解決問(wèn)題的過(guò)程也不例外,也需要一定的知識(shí)和經(jīng)驗(yàn)。為了讓計(jì)算機(jī)幫我們解決問(wèn)題,就要設(shè)計(jì)計(jì)算機(jī)能理解的算法程序。而設(shè)計(jì)算法程序的第一步就是要讓計(jì)算機(jī)理解問(wèn)題是什么。這就需要建立現(xiàn)實(shí)問(wèn)題的數(shù)學(xué)模型。建模過(guò)程就是一個(gè)對(duì)現(xiàn)實(shí)問(wèn)題的抽象過(guò)程,運(yùn)用邏輯思維能力,抓住問(wèn)題的主要因素,忽略次要因素。建立數(shù)學(xué)模型之后,第二個(gè)要考慮的問(wèn)題就是輸入輸出問(wèn)題,輸入就是將自然語(yǔ)言或人類能夠理解的其他表達(dá)方式描述的問(wèn)題轉(zhuǎn)換為數(shù)學(xué)模型中的數(shù)據(jù),輸出就是將數(shù)學(xué)模型中表達(dá)的運(yùn)算結(jié)果轉(zhuǎn)換成自然語(yǔ)言或人類能夠理解的其他表達(dá)方式。最后就是算法的設(shè)計(jì),其實(shí)就是設(shè)計(jì)一套對(duì)數(shù)學(xué)模型中的數(shù)據(jù)的操作和轉(zhuǎn)換步驟,使其能演化出最終的結(jié)果。數(shù)學(xué)模型、輸入輸出方法和算法步驟是編寫計(jì)算機(jī)算法程序的三大關(guān)鍵因素。對(duì)于非常復(fù)雜的問(wèn)題,建立數(shù)學(xué)模型是非常難的事情,比如天文物理學(xué)家研究的“宇宙大爆炸”模型,再比如熱力學(xué)研究的復(fù)雜幾何體冷卻模型,等等。不過(guò),這不是本書探討的范圍,程序員遇到的問(wèn)題更多的不是這種復(fù)雜的理論問(wèn)題,而是軟件開發(fā)過(guò)程中常用和常見的問(wèn)題,這些問(wèn)題簡(jiǎn)單,但并不枯燥乏味。對(duì)于簡(jiǎn)單的計(jì)算機(jī)算法而言,建立數(shù)學(xué)模型實(shí)際上就是設(shè)計(jì)合適的數(shù)據(jù)結(jié)構(gòu)的問(wèn)題。這又引出了前面提到的話題,數(shù)據(jù)結(jié)構(gòu)在算法設(shè)計(jì)過(guò)程中扮演著非常重要的角色。輸入輸出方式和算法步驟設(shè)計(jì)都是基于相應(yīng)的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)的,相應(yīng)的數(shù)據(jù)結(jié)構(gòu)要能很方便地將原始問(wèn)題轉(zhuǎn)換成數(shù)據(jù)結(jié)構(gòu)中的各個(gè)屬性,也要能很方便地將數(shù)據(jù)結(jié)構(gòu)中的結(jié)果以人們能夠理解的方式輸出,同時(shí),也要為算法轉(zhuǎn)換過(guò)程中各個(gè)步驟的演化提供最便利的支持。使用線性表還是關(guān)聯(lián)結(jié)構(gòu),使用樹還是圖,都是在設(shè)計(jì)輸入輸出和算法步驟時(shí)就要考慮的問(wèn)題。為什么要自己解決問(wèn)題?愛(ài)因斯坦說(shuō)過(guò):“興趣是最好的老師?!边@就是說(shuō),只要一個(gè)人對(duì)某事物產(chǎn)生興趣,就會(huì)主動(dòng)去學(xué)習(xí)、去研究,并在學(xué)習(xí)和研究的過(guò)程中產(chǎn)生愉快的情緒。我把從算法中體會(huì)到的樂(lè)趣分成三個(gè)層次:初級(jí)層次是找到特定的算法解決特定的實(shí)際問(wèn)題,這種樂(lè)趣是解決問(wèn)題后的成就感;中級(jí)層次是有些算法本身就是充滿樂(lè)趣的,搞明白這種算法的原理并寫出算法的程序代碼,能為自己今后的工作帶來(lái)便利;高級(jí)層次是自己設(shè)計(jì)算法解決問(wèn)題,讓其他人可以利用你的算法享受到初級(jí)層次的樂(lè)趣。有時(shí)候問(wèn)題可能是別人沒(méi)有遇到過(guò)的,沒(méi)有已知的解法,這種情況下只能自己解決問(wèn)題。這是本書一直強(qiáng)調(diào)算法的樂(lè)趣的原因。只有體會(huì)到樂(lè)趣,才有動(dòng)力去學(xué)習(xí)和研究,而這種學(xué)習(xí)和研究的結(jié)果是為自己帶來(lái)正向的激勵(lì),為今后的工作帶來(lái)便利。回想一下1.2.1節(jié)的例子,環(huán)形隊(duì)列相關(guān)的算法是固定長(zhǎng)度緩沖區(qū)讀寫的常用模式,如果知道這一點(diǎn),就不會(huì)有這種問(wèn)題了。1.4算法與代碼本書講到的算法都是以計(jì)算機(jī)程序作為載體展示的,其基本形式就是程序代碼。作為一個(gè)軟件開發(fā)人員,你希望看到什么樣的代碼?是這樣的代碼:doublekg=gScale*102.1+55.3;

NotifyModule1(kk);

doublekl1=kg/l_mask;

NotifyModule2(kl1);

doublekl2=kg*1.25/l_mask;

NotifyModule2(kl2);

還是這樣的代碼:doubleglobalKerp=GetGlobalKerp();

NotifyGlobalModule(globalKerp);

doublelocalKrep=globalKerp/localMask;

NotifyLocalModule(localKrep);

doublelocalKrepBoost=globalKerp*1.25/localMask;

NotifyLocalModule(localKrepBoost);

程序員都有一種直覺(jué),那就是能看懂的代碼就是好代碼。但是“能看懂”是一個(gè)非常主觀的感覺(jué),同樣的代碼給不同的人看,能否看懂有著天壤之別?!吨貥?gòu)》一書的作者為不好的代碼總結(jié)了21條“壞味道”規(guī)律,希望能夠?qū)μ?hào)入座地判斷一下代碼中的“壞代碼”。但是這21條規(guī)律仍然太主觀,于是人們又給代碼制定了很多量化指標(biāo),比如代碼注釋率(這個(gè)指標(biāo)因?yàn)闆](méi)有意義,已經(jīng)被很多組織拋棄了)、平均源代碼文件長(zhǎng)度、平均函數(shù)長(zhǎng)度、平均代碼依賴度、代碼嵌套深度、測(cè)試用例覆蓋度,等等。做這些工作的目的在于人們希望看到漂亮的代碼,這不僅僅是主觀審美的需要,更是客觀上對(duì)軟件質(zhì)量的不懈追求。漂亮的代碼有助于改善軟件的質(zhì)量,這已經(jīng)是公認(rèn)的事實(shí),因?yàn)槌绦騿T在把他們的代碼變得漂亮的過(guò)程中,能夠通過(guò)一些細(xì)小卻又重要的方式改善代碼的質(zhì)量,這些細(xì)小卻又重要的方式包括但不限于更好的設(shè)計(jì)、可測(cè)試性和可維護(hù)性等方面的方法。在保證軟件行為正確性的基礎(chǔ)上,人們都用什么詞來(lái)形容好的代碼呢?好看、漂亮、整潔、優(yōu)雅、藝術(shù)品、像詩(shī)一樣?我看過(guò)很多軟件的代碼,有開源軟件的代碼,也有商業(yè)軟件的代碼,好的代碼給我的感覺(jué)就是以上這些形容詞,當(dāng)然也見過(guò)不好的代碼,給我的感覺(jué)就是“一堆代碼”而已。我在寫“算法系列”博客專欄的時(shí)候,就特別注意這一點(diǎn),即便別人已經(jīng)發(fā)布過(guò)類似的算法實(shí)現(xiàn),我也希望我的算法呈現(xiàn)出來(lái)的是完全不一樣的代碼。設(shè)計(jì)算法也和設(shè)計(jì)軟件一樣,應(yīng)該是漂亮的代碼,如果幾百行代碼堆在一起,不分主次,關(guān)系凌亂,只是最后堆出了一個(gè)正確的結(jié)果,這不是我所希望的代碼,即虐人又虐己。大部分人來(lái)看你的博客,應(yīng)該還是為了看懂吧。在我準(zhǔn)備這本書的時(shí)候,我把很多算法又重新寫了一遍,不僅算法有趣,研究代碼也是一種樂(lè)趣。如果算法本身很有趣,但是最后的代碼實(shí)現(xiàn)卻是毫無(wú)美感的“一堆代碼”,那真是太掃興了。1.5總結(jié)本章借用了多部知名著作中對(duì)算法的定義,只是想讓大家對(duì)算法有一個(gè)“寬容”一點(diǎn)的理解。通過(guò)我親身經(jīng)歷的兩個(gè)例子,說(shuō)明了程序員與算法之間“剪不斷,理還亂”的關(guān)系。除此之外,還簡(jiǎn)單探討了算法樂(lè)趣的來(lái)源、算法和代碼的關(guān)系,以及研究代碼本身的樂(lè)趣等內(nèi)容。如果你認(rèn)同我的觀點(diǎn),就可以繼續(xù)閱讀本書了。本書的每一章都是獨(dú)立的,沒(méi)有前后關(guān)系,你可以根據(jù)自己的喜好直接閱讀相關(guān)的章節(jié)。希望本書能使你有所收獲,并體會(huì)到算法的樂(lè)趣。1.6參考資料[1]CormenTH,etal.IntroductiontoAlgorithms(SecondEdition).TheMITPress,2001[2]KnuthDE.TheArtofComputerProgramming(ThirdEdition),Vol1.Addison-Wesley,1997[3]WeissMA.DataStructuresandAlgorithmAnalysis(SecondEdition).Addison-Wesley,2001[4]OramA,WilsonG.BeautifulCode.O'ReillyMedia,Inc,2007[5]FowlerM,etal.Refactoring:ImprovingtheDesignofExistingCode.Addison-Wesley,1999第2章算法設(shè)計(jì)的基礎(chǔ)看到這里,說(shuō)明你對(duì)算法設(shè)計(jì)感興趣。編寫程序開發(fā)軟件是冒險(xiǎn)者的游戲,需要膽大心細(xì),設(shè)計(jì)一個(gè)解決實(shí)際問(wèn)題的算法尤其如此?,F(xiàn)實(shí)問(wèn)題復(fù)雜多樣,對(duì)應(yīng)的算法也是復(fù)雜多樣,形態(tài)各異,但是這些算法都遵循一些特定的方法和模式。就算法模式而言,處理各種求最優(yōu)解問(wèn)題時(shí),人們常用貪婪法、動(dòng)態(tài)規(guī)劃法等算法模式;處理迷宮類問(wèn)題時(shí),窮盡式的枚舉和回溯是常用的模式。就算法的實(shí)現(xiàn)方法而言,如果算法需要頻繁地查表操作,那么數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)通常會(huì)選擇有序表來(lái)實(shí)現(xiàn);反過(guò)來(lái),當(dāng)設(shè)計(jì)的算法用到了樹和圖這樣的數(shù)據(jù)結(jié)構(gòu)時(shí),含有遞歸結(jié)構(gòu)的方法就常常伴隨它們左右。算法設(shè)計(jì)是個(gè)復(fù)雜的內(nèi)容,單就這個(gè)話題就可以寫一本書了??肆植竦摹端惴ㄔO(shè)計(jì)》就是一本這樣的書,掌握了算法設(shè)計(jì)的基本內(nèi)容之后,就可以去啃《算法導(dǎo)論》或其他各種競(jìng)賽類算法的書了,但這都不是本書的重點(diǎn)。本書的目的是展示算法的有趣之處,希望通過(guò)一些簡(jiǎn)單有趣的算法改變程序員對(duì)算法的固有印象,我們不去研究那些各個(gè)行業(yè)領(lǐng)域內(nèi)的復(fù)雜算法,只來(lái)討論一些通用的、共性的東西。2.1程序的基本結(jié)構(gòu)從大的方面來(lái)考量算法問(wèn)題,相對(duì)于并行算法,本書介紹的都是串行算法的設(shè)計(jì)方法。按照馮·諾依曼計(jì)算機(jī)體系的設(shè)計(jì),計(jì)算機(jī)的CPU每次只能串行執(zhí)行一條指令,即使那些號(hào)稱支持多線程的操作系統(tǒng),其實(shí)際效果也是“宏觀上并行,微觀上串行”。順序執(zhí)行、循環(huán)和分支跳轉(zhuǎn)是程序設(shè)計(jì)的三大基本結(jié)構(gòu),算法也是程序,千姿百態(tài)的算法也是由這三大基礎(chǔ)結(jié)構(gòu)構(gòu)成的。2.1.1順序執(zhí)行順序執(zhí)行是算法的基礎(chǔ)結(jié)構(gòu),循環(huán)體結(jié)構(gòu)的每個(gè)循環(huán)體內(nèi)也是順序執(zhí)行的,分支和跳轉(zhuǎn)的每個(gè)分支內(nèi)也是順序執(zhí)行的。假如算法中某個(gè)操作需要幾個(gè)步驟完成,每個(gè)步驟都依賴于前一個(gè)步驟,將前一個(gè)步驟的輸出作為下一個(gè)步驟的輸入,中間不能打斷和調(diào)整順序,這樣的結(jié)構(gòu)就是算法的順序執(zhí)行結(jié)構(gòu)(如圖2-1所示)。圖2-1順序執(zhí)行結(jié)構(gòu)圖以雇員工資打印算法為例,必須先統(tǒng)計(jì)出雇員的出勤情況,然后才能計(jì)算工資,最后才能打印工資單,這三個(gè)步驟順序不能打亂,否則將無(wú)法得到正確的結(jié)果。第11章中計(jì)算太陽(yáng)的地心視黃經(jīng)算法,需要計(jì)算出平黃經(jīng),然后修正地心章動(dòng),修正交角章動(dòng),修正光行差,最后得到地心視黃經(jīng)。其中修正地心章動(dòng)、修正交角章動(dòng)和修正光行差這三個(gè)步驟是對(duì)平黃經(jīng)值的修正,沒(méi)有前后關(guān)系,可以互換順序,雖然算法實(shí)現(xiàn)是順序方式,但是它們不是嚴(yán)格的順序執(zhí)行結(jié)構(gòu)。順序執(zhí)行結(jié)構(gòu)雖然簡(jiǎn)單,但是卻具有重要的意義。算法的基本特征之一就是確定性,因此算法里的順序結(jié)構(gòu)必須是明確的,其結(jié)果是不變的。除了確定性,順序執(zhí)行結(jié)構(gòu)還具有封閉性特征,也就是說(shuō),無(wú)論上一步的結(jié)果如何,都會(huì)繼續(xù)執(zhí)行下一步,不受外部條件和內(nèi)部因素的影響。2.1.2循環(huán)結(jié)構(gòu)循環(huán)結(jié)構(gòu)也是算法中一種很重要的控制流程,循環(huán)被定義為在算法中只出現(xiàn)一次但是卻有可能被執(zhí)行多次的一段邏輯體。從實(shí)際應(yīng)用角度看,稍微復(fù)雜一點(diǎn)的算法都會(huì)用到循環(huán)結(jié)構(gòu)。循環(huán)結(jié)構(gòu)一般由三部分組成:循環(huán)初始化、循環(huán)體和循環(huán)條件(退出條件)。循環(huán)初始化部分一般做些循環(huán)體控制狀態(tài)的初始化工作,包括循環(huán)條件的初始化。循環(huán)體可以是順序執(zhí)行結(jié)構(gòu),也可以帶有分支跳轉(zhuǎn)結(jié)構(gòu),甚至可以是個(gè)循環(huán)體(多重循環(huán)結(jié)構(gòu))。循環(huán)條件可以是簡(jiǎn)單的計(jì)數(shù)器,也可以是復(fù)雜的邏輯判斷,它定義循環(huán)的執(zhí)行條件或退出條件。有少數(shù)編程語(yǔ)言提供一些特殊的指令,可以控制循環(huán)結(jié)構(gòu)的流程和退出,比如C語(yǔ)言的continue和break語(yǔ)句,但是這種控制方式不具備算法通用性。從數(shù)據(jù)結(jié)構(gòu)方面看,涉及線性表的遍歷和查找操作,一般都會(huì)用到循環(huán)結(jié)構(gòu),比如多項(xiàng)式求和算法和各種排序算法。維基百科的“算法”條目給出了一個(gè)求最大數(shù)的例子,其算法實(shí)現(xiàn)如下。intmax(int*values,intsize)

{

intmval=*values;

inti;

for(i=1;i<size;i++)

{

if(values[i]>mval)

mval=values[i];

}

returnmval;

}

for語(yǔ)句塊的代碼就是C語(yǔ)言形式的循環(huán)結(jié)構(gòu),循環(huán)條件是i<size。關(guān)于循環(huán)也有很多有趣的話題。舉個(gè)例子,如果算法操作的數(shù)據(jù)結(jié)構(gòu)是二維數(shù)組,通常都會(huì)用到兩重循環(huán),但是也可以用單循環(huán)遍歷二維數(shù)組,第19章介紹數(shù)獨(dú)游戲的解法的時(shí)候,對(duì)小九宮格的遍歷就多次使用了這種技巧,比如初始化一個(gè)小九宮格的代碼可能是這樣的:for(inti=0;i<9;i++)

{

introw=i/3;

intcol=i%3;

game->cells[row][col].fixed=false;

}

只要介紹循環(huán),就不得不提遞歸,一些文獻(xiàn)資料將遞歸作為一種獨(dú)立的程序控制方法介紹,但是更多的資料將遞歸看作是循環(huán)的一種替代形式。這兩種形式看起來(lái)差異很大,但是本質(zhì)都是一樣的,遞歸結(jié)構(gòu)通常都可以用復(fù)雜一點(diǎn)的循環(huán)形式代替,特別是尾遞歸形式,可以直接替換成循環(huán)結(jié)構(gòu)。遞歸結(jié)構(gòu)一般由遞歸關(guān)系定義和遞歸終止條件兩部分組成,遞歸關(guān)系定義就是對(duì)問(wèn)題的分解,是指向遞歸終止條件轉(zhuǎn)化的規(guī)則,而遞歸終止條件通常就是問(wèn)題分解到最小規(guī)模時(shí),這個(gè)最小規(guī)模的問(wèn)題對(duì)應(yīng)的結(jié)果。遞歸方法符合人類思考問(wèn)題的方式,它可以使算法結(jié)構(gòu)簡(jiǎn)單,過(guò)程簡(jiǎn)潔。對(duì)于樹和圖這樣的數(shù)據(jù)結(jié)構(gòu),遞歸方法更是具有循環(huán)形式無(wú)法比擬的優(yōu)勢(shì),下面給出了FindTNode()函數(shù)遞歸方式實(shí)現(xiàn)的二叉樹查找算法,大家可以體會(huì)一下遞歸的優(yōu)美。boolFindTNode(TNODE*tr,intkey)

{

if(tr==NULL)

returnfalse;

if(tr->key==key)

returntrue;

if(key<tr->key)

returnFindTNode(tr->left,key);

else

returnFindTNode(tr->right,key);

}

尾遞歸是尾調(diào)用1的一種特殊情況,也是遞歸結(jié)構(gòu)的一種特殊形式。編譯器一般都可以對(duì)尾遞歸進(jìn)行優(yōu)化(尾調(diào)用消除技術(shù)),直接利用當(dāng)前函數(shù)的棧幀,將尾調(diào)用處理成循環(huán)的形式。實(shí)際上,即便不使用編譯器的優(yōu)化,尾遞歸也可以很容易轉(zhuǎn)化成循環(huán)形式。前文給出的FindTNode()函數(shù)其實(shí)就是尾遞歸,可以很容易將其轉(zhuǎn)化成循環(huán)形式,代碼如下所示:1尾調(diào)用是指一個(gè)函數(shù)里的最后一個(gè)動(dòng)作是調(diào)用一個(gè)函數(shù)的情形,這個(gè)函數(shù)調(diào)用的返回值直接被當(dāng)前函數(shù)作為返回值。boolFindTNode(TNODE*tr,intkey)

{

TNODE*curNode=tr;

while(curNode!=NULL)

{

if(curNode->key==key)

returntrue;

if(key<curNode->key)

curNode=curNode->left;

else

curNode=curNode->right;

}

returnfalse;

}

遞歸結(jié)構(gòu)使用的函數(shù)遞歸調(diào)用,會(huì)增加任務(wù)的棧空間使用,用遞歸方法解決問(wèn)題的規(guī)模受系統(tǒng)棧空間的約束,除此之外,函數(shù)調(diào)用時(shí)的參數(shù)入棧和出棧也會(huì)降低算法的效率。2.1.3分支和跳轉(zhuǎn)結(jié)構(gòu)順序結(jié)構(gòu)可以解決計(jì)算、輸入和輸出等問(wèn)題,但是不能作判斷和選擇。現(xiàn)實(shí)生活中的很多問(wèn)題都需要進(jìn)行判斷和選擇,處理這些問(wèn)題,關(guān)鍵在于對(duì)條件的判斷。分支和跳轉(zhuǎn)結(jié)構(gòu)在程序中扮演著重要的角色,正是由于有了分支和跳轉(zhuǎn),程序才能夠產(chǎn)生多種多樣的結(jié)果。算法設(shè)計(jì)也離不開分支和跳轉(zhuǎn)結(jié)構(gòu),根據(jù)對(duì)條件的判斷,選擇合適的處理步驟,是算法實(shí)現(xiàn)過(guò)程中常用的邏輯。分支和跳轉(zhuǎn)結(jié)構(gòu)算法設(shè)計(jì)的關(guān)鍵是設(shè)計(jì)分支條件和算法的跳轉(zhuǎn)流程,一般一個(gè)分支條件對(duì)應(yīng)一個(gè)處理流程。算法在執(zhí)行的過(guò)程中,根據(jù)構(gòu)造的分支條件進(jìn)行判斷,根據(jù)判斷的結(jié)果轉(zhuǎn)入相應(yīng)的處理流程繼續(xù)執(zhí)行。根據(jù)跳轉(zhuǎn)分支的個(gè)數(shù),分支結(jié)構(gòu)又可細(xì)分為單分支結(jié)構(gòu)、雙分支結(jié)構(gòu)、嵌套分支結(jié)構(gòu)和多分支結(jié)構(gòu)(switch-case結(jié)構(gòu))。單分支結(jié)構(gòu)一般可表示為:if(條件)

{

分支流程

}

根據(jù)分支條件的判斷結(jié)果,{}內(nèi)的分支流程可能被執(zhí)行,也可能不被執(zhí)行。從算法構(gòu)造的角度看,單分支結(jié)構(gòu)多被用在根據(jù)條件判斷,需要在正常處理流程中插入一些特殊處理的情況。比如,計(jì)算一年有多少天,如果是閏年就需要額外多加一天,就可以采用單分支結(jié)構(gòu),代碼如下所示。intdays_per_year=365;

if(((year%4==0)&&(year%100!=0))||(year%400==0))

{

days_per_year+=1;

}

returndays_per_year;

雙分支結(jié)構(gòu)適合那種非“真”即“假”的流程處理,兩個(gè)分支為互斥流程,執(zhí)行一個(gè)分支就必然不會(huì)執(zhí)行另一個(gè)分支。雙分支結(jié)構(gòu)一般可表示為:if(條件)

{

分支流程1

}

else

{

分支流程2

}

分支流程1和分支流程2是根據(jù)條件互斥執(zhí)行的。單分支結(jié)構(gòu)有時(shí)候可以看作是雙分支結(jié)構(gòu)的一種特殊情況,即分支流程2是空的情況。當(dāng)某個(gè)條件分支的處理流程中又包含分支條件時(shí),就構(gòu)成嵌套分支結(jié)構(gòu),嵌套分支結(jié)構(gòu)可以表示為:if(條件1)

{

if(條件2)

{

分支流程

}

}

當(dāng)算法的某個(gè)流程處理存在多級(jí)條件判斷的時(shí)候,就會(huì)用上嵌套分支結(jié)構(gòu),但是過(guò)深的嵌套分支結(jié)構(gòu)會(huì)使得算法代碼條理不清楚,降低代碼的可讀性。一般嵌套分支結(jié)構(gòu)不要超過(guò)三層,否則的話就要考慮調(diào)整算法,或者用函數(shù)封裝替換分支代碼,以提高算法代碼的可讀性。對(duì)于簡(jiǎn)單條件的多層嵌套,可以使用組合條件來(lái)避免多層分支嵌套,比如前面的兩層嵌套結(jié)構(gòu)就可以調(diào)整為:if(條件1&&條件2)

{

分支流程

}

當(dāng)算法的某個(gè)步驟需要多重篩選條件時(shí),就會(huì)用到多分支結(jié)構(gòu),多分支結(jié)構(gòu)可以表示為:if(條件1)

{

分支流程1

}

elseif(條件2)

{

分支流程2

}

...

else

{

分支流程n

}

和雙分支結(jié)構(gòu)一樣,多分支結(jié)構(gòu)的每個(gè)分支流程也是互斥執(zhí)行的。生活中有很多情況都不是非真即假的兩種選擇,因此多分支結(jié)構(gòu)在算法中也經(jīng)常用到,比如給學(xué)生打評(píng)語(yǔ)的算法,就需要根據(jù)分?jǐn)?shù)的區(qū)間將評(píng)語(yǔ)定位為“優(yōu)秀”、“優(yōu)良”、“良好”、“及格”,等等。有一些編程語(yǔ)言提供了類似于switch-case的結(jié)構(gòu),可以在某些情況下替換多分支結(jié)構(gòu)。switch-case結(jié)構(gòu)的優(yōu)點(diǎn)是對(duì)分支條件只進(jìn)行一次判斷就可以決定代碼的分支流程,避免多次判斷分支條件,但是缺點(diǎn)就是不能進(jìn)行復(fù)雜的條件判斷,比如C語(yǔ)言的switch-case結(jié)構(gòu),其分支判斷條件就只支持整數(shù)類型的常量表達(dá)式。過(guò)長(zhǎng)的多分支結(jié)構(gòu)常被視為軟件中的不良結(jié)構(gòu),因?yàn)樗`背了OCP原則(開放、封閉原則),每當(dāng)需要新增一種條件判斷處理時(shí),就要新增一個(gè)if-else分支。在很多情況下,使用函數(shù)表結(jié)構(gòu)是避免過(guò)長(zhǎng)的多分支結(jié)構(gòu)的有效方法,下面就是“狼、羊和菜過(guò)河”問(wèn)題的求解算法中用函數(shù)表結(jié)構(gòu)代替過(guò)長(zhǎng)的多分支結(jié)構(gòu)的例子。求解“狼、羊和菜過(guò)河”問(wèn)題,從一個(gè)狀態(tài)過(guò)渡到下一個(gè)狀態(tài)是由農(nóng)夫的動(dòng)作驅(qū)動(dòng)的,農(nóng)夫一共可以采取8種動(dòng)作,每種動(dòng)作都對(duì)應(yīng)一個(gè)狀態(tài)轉(zhuǎn)變處理流程。如果采用if-else多分支結(jié)構(gòu),處理狀態(tài)轉(zhuǎn)換的代碼將會(huì)非常長(zhǎng),為了避免過(guò)長(zhǎng)的分支跳轉(zhuǎn)代碼,算法采用了函數(shù)表結(jié)構(gòu)。首先聲明函數(shù)表項(xiàng)的定義:typedefbool(*ProcessNextFuncPtr)(constItemState¤t,ItemState&next);

structActionProcess

{

Actionact;

ProcessNextFuncPtrprocessFunc;

};

然后分別為農(nóng)夫的8個(gè)動(dòng)作指定處理函數(shù),得到函數(shù)表的定義:ActionProcessactMap[]=

{

{FARMER_GO,ProcessFarmerGo},

{FARMER_GO_TAKE_WOLF,ProcessFarmerGoTakeWolf},

{FARMER_GO_TAKE_SHEEP,ProcessFarmerGoTakeSheep},

{FARMER_GO_TAKE_VEGETABLE,ProcessFarmerGoTakeVegetable},

{FARMER_BACK,ProcessFarmerBack},

{FARMER_BACK_TAKE_WOLF,ProcessFarmerBackTakeWolf},

{FARMER_BACK_TAKE_SHEEP,ProcessFarmerBackTakeSheep},

{FARMER_BACK_TAKE_VEGETABLE,ProcessFarmerBackTakeVegetable}

};

如果用if-else結(jié)構(gòu),處理狀態(tài)轉(zhuǎn)換可能需要30多行代碼,而利用這個(gè)函數(shù)表,處理狀態(tài)轉(zhuǎn)換的代碼只有幾行:ItemStatenext;

for(inti=0;i<sizeof(actMap)/sizeof(actMap[0]);i++)

{

if(actMap[i].act==action)

{

actMap[i].processFunc(current,next);

break;

}

}

如果將這個(gè)函數(shù)表存入一個(gè)關(guān)聯(lián)容器(比如std::map)中,則循環(huán)體的代碼都可以省去。如果隨著算法演化,有新的動(dòng)作需要處理,則只需要在函數(shù)表中添加新的條目即可,狀態(tài)轉(zhuǎn)換的代碼不需要做任何改動(dòng)。算法需要確定性,分支和跳轉(zhuǎn)看似使得算法具有不確定性,但是實(shí)際上,分支的判斷和選擇都是在所有已確定處理流程的框架中進(jìn)行的,也就是說(shuō),這些選擇都是算法確定范圍之內(nèi)的選擇,對(duì)算法確定性沒(méi)有影響。雖然分支和跳轉(zhuǎn)是算法構(gòu)造的基礎(chǔ)結(jié)構(gòu),但是如果能采用精心設(shè)計(jì)的一致性處理邏輯避免分支和跳轉(zhuǎn),通常算法會(huì)具有更好的結(jié)構(gòu)。前面提到的函數(shù)表就是一個(gè)一致性處理的例子,第1章提到的環(huán)形隊(duì)列的例子中,對(duì)尾指針移動(dòng)的處理也是一個(gè)例子,如果不采用對(duì)N取模的一致性處理,則每次指針移動(dòng)時(shí)都要做是否已經(jīng)到表尾的判斷處理。2.2算法實(shí)現(xiàn)與數(shù)據(jù)結(jié)構(gòu)計(jì)算機(jī)體系中的數(shù)據(jù),是指能被計(jì)算機(jī)識(shí)別和處理的各種符號(hào)的總稱。人類所能識(shí)別的各種數(shù)據(jù),比如文字、語(yǔ)言和圖像,在計(jì)算機(jī)內(nèi)都是以二進(jìn)制形式存在的,但是這些二進(jìn)制數(shù)據(jù)之間存在著各種組織關(guān)系。我們通常說(shuō)的數(shù)據(jù)結(jié)構(gòu),其實(shí)包含了兩層意思,一是指相互之間存在某種特定關(guān)系的數(shù)據(jù)的集合,二是指數(shù)據(jù)之間的相互關(guān)系,也就是數(shù)據(jù)的邏輯結(jié)構(gòu)。因此,當(dāng)我們說(shuō)定義數(shù)據(jù)結(jié)構(gòu)時(shí),除了定義數(shù)據(jù)之間的相互關(guān)系,還包括根據(jù)這些關(guān)系組織在一起的數(shù)據(jù)。在建立數(shù)學(xué)模型的階段,我們說(shuō)的數(shù)據(jù)結(jié)構(gòu)更偏重于定義數(shù)據(jù)之間的相互關(guān)系,設(shè)計(jì)具體的算法步驟時(shí),考慮的是如何對(duì)構(gòu)建在這些數(shù)據(jù)關(guān)系之上的實(shí)際數(shù)據(jù)進(jìn)行加工和處理。算法和數(shù)據(jù)結(jié)構(gòu)關(guān)系緊密,數(shù)據(jù)結(jié)構(gòu)是算法設(shè)計(jì)的基礎(chǔ),不合適的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì),有可能導(dǎo)致無(wú)法設(shè)計(jì)算法的演算步驟,從而無(wú)法實(shí)現(xiàn)算法。數(shù)據(jù)之間常見的邏輯結(jié)構(gòu)包括線性結(jié)構(gòu)、關(guān)聯(lián)結(jié)構(gòu)(集合、映射)、樹形結(jié)構(gòu)和圖形結(jié)構(gòu),也有一些資料將樹形結(jié)構(gòu)看作是圖形結(jié)構(gòu)的一種特殊形式,但是因?yàn)檫@兩種數(shù)據(jù)結(jié)構(gòu)在數(shù)據(jù)的組織和定義方式上存在很大的差異,更多的資料還是將它們分為兩種結(jié)構(gòu)。接下來(lái)我們討論一下算法設(shè)計(jì)常用的幾種基本數(shù)據(jù)結(jié)構(gòu),對(duì)于簡(jiǎn)單的問(wèn)題,應(yīng)用這些基本數(shù)據(jù)結(jié)構(gòu)就可以解決,但是對(duì)于復(fù)雜的算法,往往需要將這些基本的數(shù)據(jù)結(jié)構(gòu)組合起來(lái)形成更復(fù)雜的邏輯結(jié)構(gòu)。2.2.1基本數(shù)據(jù)結(jié)構(gòu)在算法設(shè)計(jì)中的應(yīng)用線性表是數(shù)據(jù)結(jié)構(gòu)中最簡(jiǎn)單的基本數(shù)據(jù)結(jié)構(gòu)。線性表的使用和維護(hù)都很簡(jiǎn)單,這一特點(diǎn)使其成為很多算法的基礎(chǔ)。數(shù)組、鏈表、棧和隊(duì)列是四種最常見的線性表,其外部行為和接口都各有特色,本節(jié)就簡(jiǎn)單介紹一下這四種基本數(shù)據(jù)結(jié)構(gòu)的特點(diǎn)及其在算法設(shè)計(jì)中的應(yīng)用。1.數(shù)組數(shù)組(array)最一種相對(duì)比較簡(jiǎn)單的數(shù)據(jù)組織關(guān)系,所有數(shù)據(jù)元素存儲(chǔ)在一片連續(xù)的區(qū)域內(nèi)。對(duì)數(shù)組的訪問(wèn)方式一般是通過(guò)下標(biāo)直接訪問(wèn)數(shù)組元素,除此之外,對(duì)數(shù)組的基本操作還有插入、刪除和查找。數(shù)組元素的直接訪問(wèn)幾乎沒(méi)有開銷,但是插入和刪除操作需要移動(dòng)數(shù)組元素,開銷比較大,因此在插入和刪除操作比較頻繁的場(chǎng)合下,不適合使用數(shù)組。在數(shù)組中查找一個(gè)元素的時(shí)間復(fù)雜度是O(n),如果數(shù)組元素是有序存儲(chǔ)的,則使用二分查找可以將時(shí)間復(fù)雜度降為O(lgn)。在數(shù)組中存儲(chǔ)的數(shù)組元素,除了數(shù)組元素的值需要關(guān)注之外,數(shù)組元素的下標(biāo)也是一個(gè)很有用的屬性,有時(shí)候可以巧妙地利用下標(biāo)簡(jiǎn)化一些算法的實(shí)現(xiàn)方式。例如,有若干個(gè)數(shù)存放在value數(shù)組中,這些數(shù)的取值范圍是[1-100],請(qǐng)?jiān)O(shè)計(jì)一個(gè)算法統(tǒng)計(jì)一下這些數(shù)中相同的數(shù)出現(xiàn)的次數(shù)。經(jīng)分析發(fā)現(xiàn),雖然value中數(shù)字的個(gè)數(shù)很多,但是范圍并不大,可以設(shè)計(jì)一個(gè)有100個(gè)元素的數(shù)組,數(shù)組元素的下標(biāo)對(duì)應(yīng)數(shù)字,數(shù)組元素的值就是對(duì)應(yīng)數(shù)字出現(xiàn)的次數(shù),只需如下兩行代碼即可完成統(tǒng)計(jì)工作:for(inti=0;i<count;i++)

numCount[values[i]-1]++;

雖然對(duì)于那些沒(méi)有出現(xiàn)過(guò)的數(shù)字也需要占用numCount[]數(shù)組的一個(gè)位置,但是這點(diǎn)空間上的開銷是可以接受的。相對(duì)于固定長(zhǎng)度的數(shù)組,還有可變長(zhǎng)度的數(shù)組,比如C++的STL提供的std::vector,可變長(zhǎng)數(shù)組具有數(shù)組的訪問(wèn)效率,同時(shí)長(zhǎng)度可隨著數(shù)據(jù)元素的增加而變長(zhǎng),使用場(chǎng)合比定長(zhǎng)數(shù)組靈活。除了用一維數(shù)組表示線性表之外,還可以用多維數(shù)組表示更復(fù)雜的局面,比如棋盤類游戲可以使用二維數(shù)組表示棋盤狀態(tài),魔方類的游戲還可能用到三維數(shù)組,需要根據(jù)問(wèn)題的情況靈活運(yùn)用。2.鏈表在線性表的長(zhǎng)度不能確定的場(chǎng)合,一般會(huì)采用鏈表(linkedlist)的形式。鏈表結(jié)構(gòu)的每個(gè)節(jié)點(diǎn)數(shù)據(jù)都由兩個(gè)域組成,一個(gè)是存放實(shí)際數(shù)據(jù)元素的數(shù)據(jù)域,另一個(gè)就是構(gòu)成鏈?zhǔn)浇Y(jié)構(gòu)的指針域。對(duì)于單向鏈表,指針域只有一個(gè)后向指針,對(duì)于雙向鏈表,指針域由一個(gè)后向指針和一個(gè)前向指針組成。鏈表的插入和刪除只需要修改指針域的指針指向即可完成,比數(shù)組的插入和刪除操作效率高,但是訪問(wèn)數(shù)據(jù)元素的效率比較低,需要從鏈表頭部向后(或向前)搜索,查找操作的時(shí)間復(fù)雜度是O(n)。理論上鏈表的長(zhǎng)度是不受限制的,實(shí)際使用鏈表時(shí),常受存儲(chǔ)器空間的限制,使得鏈表長(zhǎng)度也不能無(wú)限增長(zhǎng),但是鏈表長(zhǎng)度可動(dòng)態(tài)變化這一點(diǎn),比數(shù)組具有很大的優(yōu)勢(shì)。單向鏈表只能在一個(gè)方向上遍歷鏈表節(jié)點(diǎn),從一個(gè)節(jié)點(diǎn)開始遍歷到鏈表的尾部節(jié)點(diǎn)就停止。雙向鏈表可以從兩個(gè)方向遍歷鏈表節(jié)點(diǎn),從一個(gè)節(jié)點(diǎn)開始,向前遍歷到鏈表頭部節(jié)點(diǎn)停止,向后遍歷到鏈表尾部節(jié)點(diǎn)停止。在某些應(yīng)用場(chǎng)合,還可以將鏈表尾部節(jié)點(diǎn)的后向指針指向鏈表頭部節(jié)點(diǎn)(對(duì)于雙向鏈表,其頭部節(jié)點(diǎn)的前向指針同時(shí)指向鏈表的尾部節(jié)點(diǎn)),構(gòu)成一個(gè)環(huán)形鏈表。環(huán)形鏈表中頭部節(jié)點(diǎn)和尾部節(jié)點(diǎn)的概念已經(jīng)弱化,從任何一個(gè)節(jié)點(diǎn)開始都可以遍歷整個(gè)鏈表。鏈表的頭節(jié)點(diǎn)作為整個(gè)鏈表遍歷的起點(diǎn),是一個(gè)比較特殊的節(jié)點(diǎn),需要特殊處理,尤其是在插入和刪除節(jié)點(diǎn)的時(shí)候。如果節(jié)點(diǎn)插入在頭節(jié)點(diǎn)之前,或者刪除的節(jié)點(diǎn)就是頭節(jié)點(diǎn),就需要調(diào)整鏈表頭節(jié)點(diǎn)的指針,否則的話,仍用原來(lái)保存的頭節(jié)點(diǎn)訪問(wèn)鏈表,就可能跳過(guò)新插入的節(jié)點(diǎn),或操作已經(jīng)失效的指針。為了解決這個(gè)問(wèn)題,人們?cè)O(shè)計(jì)了一種在鏈表中使用固定頭節(jié)點(diǎn)的方法,這個(gè)固定的頭節(jié)點(diǎn)稱為“表頭節(jié)點(diǎn)”,也被稱為“啞節(jié)點(diǎn)”(dummynode),《算法導(dǎo)論》一書將其稱為“哨兵節(jié)點(diǎn)”。表頭節(jié)點(diǎn)可以是一個(gè)沒(méi)有數(shù)據(jù)域、只有指針域的特殊節(jié)點(diǎn),也可以是和其他節(jié)點(diǎn)類型一樣的節(jié)點(diǎn)(數(shù)據(jù)域不使用),更多的情況是在表頭節(jié)點(diǎn)的數(shù)據(jù)域中放置一些與鏈表有關(guān)的狀態(tài)信息,比如當(dāng)前鏈表中的數(shù)據(jù)元素節(jié)點(diǎn)個(gè)數(shù)。表頭節(jié)點(diǎn)的指針域始終指向第一個(gè)實(shí)際鏈表節(jié)點(diǎn),如果表頭節(jié)點(diǎn)的指針域是NULL,則表示這個(gè)鏈表是空表。使用表頭節(jié)點(diǎn)的好處有兩個(gè),一個(gè)好處是無(wú)論鏈表是否為空表,始終有一個(gè)能標(biāo)識(shí)鏈表的頭節(jié)點(diǎn),可以用一致的方法處理空鏈表和非空鏈表;另一個(gè)好處是對(duì)鏈表進(jìn)行插入、刪除和遍歷操作時(shí),不需要對(duì)數(shù)據(jù)元素的首節(jié)點(diǎn)和中間節(jié)點(diǎn)做差異處理,對(duì)每個(gè)節(jié)點(diǎn)的操作可以做到一致性。除了查找和訪問(wèn)的效率沒(méi)有數(shù)組高之外,鏈表的每個(gè)節(jié)點(diǎn)都要額外存儲(chǔ)一個(gè)指針域,因此需要一定的存儲(chǔ)開銷。對(duì)于一些插入和刪除操作比較少,查找、遍歷操作比較多的場(chǎng)合,應(yīng)該優(yōu)先選擇使用可變長(zhǎng)數(shù)組代替鏈表。3.棧棧(stack)是一種特殊的線性表,其特殊性在于只能在表的一端插入和刪除數(shù)組元素,插入和刪除動(dòng)作分別被稱為“入?!焙汀俺鰲!?。嚴(yán)格來(lái)說(shuō),棧不是一種數(shù)據(jù)存儲(chǔ)方式,而是一種邏輯管理方式,它遵循“后進(jìn)先出”(LastInFirstOut)的原則管理和維護(hù)表中的數(shù)據(jù)。棧的數(shù)據(jù)存儲(chǔ)方式可以采用數(shù)組,也可以使用鏈表,分別被稱為“順序?!焙汀版?zhǔn)綏!?,但是無(wú)論采用何種存儲(chǔ)方式,其外部行為都是一樣的,即只能通過(guò)“出棧”和“入?!钡姆绞皆跀?shù)據(jù)表的一端操作數(shù)據(jù)。棧是一種非常有用的數(shù)據(jù)結(jié)構(gòu),利用棧的一些特性,可以將某算法的遞歸實(shí)現(xiàn)轉(zhuǎn)換成非遞歸實(shí)現(xiàn),在使用窮盡搜索方法時(shí),也會(huì)使用棧保存當(dāng)前的狀態(tài),有時(shí)候,廣度優(yōu)先搜索和深度優(yōu)先搜索的差異僅僅是使用棧還是使用隊(duì)列。下面是一個(gè)判斷表達(dá)式的括號(hào)是否匹配的小算法,可以體會(huì)一下這種“先進(jìn)后出”的數(shù)據(jù)結(jié)構(gòu)的特點(diǎn)。boolIsMatchBrackets(conststd::string&express)

{

std::stack<std::string::value_type>epStk;

std::string::size_typei;

for(i=0;i<express.length();i++)

{

if(IsLeftBrackets(express[i]))

{

epStk.push(express[i]);

}

if(IsRightBrackets(express[i]))

{

if(epStk.empty())

returnfalse;

epStk.pop();

}

}

returnepStk.empty();

}

4.隊(duì)列隊(duì)列(queue)也是一種特殊的線性表,普通的隊(duì)列只能在表的一端插入數(shù)據(jù),在另一端刪除數(shù)據(jù),不能在隊(duì)列的其他位置插入和刪除數(shù)據(jù)。插入和刪除動(dòng)作分別被稱為“入隊(duì)”和“出隊(duì)”,能執(zhí)行“入隊(duì)”的一端稱為“后端”(rear),能執(zhí)行出隊(duì)的一端稱為“前端”(front)。與棧一樣,隊(duì)列也不是一種數(shù)據(jù)存儲(chǔ)方式,而是一種邏輯管理方式,它遵循“先進(jìn)先出(FirstInFirstOut)”的原則管理和維護(hù)表中的數(shù)據(jù)。隊(duì)列的數(shù)據(jù)存儲(chǔ)方式可以采用數(shù)組,也可以使用鏈表。隊(duì)列有多種使用

溫馨提示

  • 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

最新文檔

評(píng)論

0/150

提交評(píng)論