算法的樂趣(廣泛涵蓋常用算法結構及應用)_第1頁
算法的樂趣(廣泛涵蓋常用算法結構及應用)_第2頁
算法的樂趣(廣泛涵蓋常用算法結構及應用)_第3頁
算法的樂趣(廣泛涵蓋常用算法結構及應用)_第4頁
算法的樂趣(廣泛涵蓋常用算法結構及應用)_第5頁
已閱讀5頁,還剩538頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

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

{

/*緩沖區已滿,向前移動950條記錄,為新紀錄騰出50條記錄的空間*/

memmove(s_EponSyncLog.syncLogs,

s_EponSyncLog.syncLogs+SYNC_LOG_MEMOVER_CNT,

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

/*清空新騰出來的空間*/

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

0,SYNC_LOG_MEMOVER_CNT*sizeof(EPON_SYNC_LOG_DATA));

/*寫入當前一條日志*/

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;

}

/*如果緩沖區有空間,則直接寫入當前一條記錄*/

memmove(s_EponSyncLog.syncLogs+syncLogCnt,

pLogData,sizeof(EPON_SYNC_LOG_DATA));

s_EponSyncLog.logCnt++;

}

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

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

{

POINTpts[MAX_DIRTY_POINT];/*記錄搜索過的前5個點的位置*/

intcount;

}RESULT;

這個數據結構有兩個屬性,count是搜索過程中發現的相連點的個數,pts是記錄這些相連點位置的線性表。記錄這些點的位置是為了在搜索結束后,如果判定這些點是污點,可以利用這些記錄的位置信息直接清除這些點的顏色。/*8個方向*/

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個點的位置*/

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

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

{

intmval=*values;

inti;

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

{

if(values[i]>mval)

mval=values[i];

}

returnmval;

}

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

{

introw=i/3;

intcol=i%3;

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

}

只要介紹循環,就不得不提遞歸,一些文獻資料將遞歸作為一種獨立的程序控制方法介紹,但是更多的資料將遞歸看作是循環的一種替代形式。這兩種形式看起來差異很大,但是本質都是一樣的,遞歸結構通常都可以用復雜一點的循環形式代替,特別是尾遞歸形式,可以直接替換成循環結構。遞歸結構一般由遞歸關系定義和遞歸終止條件兩部分組成,遞歸關系定義就是對問題的分解,是指向遞歸終止條件轉化的規則,而遞歸終止條件通常就是問題分解到最小規模時,這個最小規模的問題對應的結果。遞歸方法符合人類思考問題的方式,它可以使算法結構簡單,過程簡潔。對于樹和圖這樣的數據結構,遞歸方法更是具有循環形式無法比擬的優勢,下面給出了FindTNode()函數遞歸方式實現的二叉樹查找算法,大家可以體會一下遞歸的優美。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);

}

尾遞歸是尾調用1的一種特殊情況,也是遞歸結構的一種特殊形式。編譯器一般都可以對尾遞歸進行優化(尾調用消除技術),直接利用當前函數的棧幀,將尾調用處理成循環的形式。實際上,即便不使用編譯器的優化,尾遞歸也可以很容易轉化成循環形式。前文給出的FindTNode()函數其實就是尾遞歸,可以很容易將其轉化成循環形式,代碼如下所示:1尾調用是指一個函數里的最后一個動作是調用一個函數的情形,這個函數調用的返回值直接被當前函數作為返回值。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;

}

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

{

分支流程

}

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

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

{

days_per_year+=1;

}

returndays_per_year;

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

{

分支流程1

}

else

{

分支流程2

}

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

{

if(條件2)

{

分支流程

}

}

當算法的某個流程處理存在多級條件判斷的時候,就會用上嵌套分支結構,但是過深的嵌套分支結構會使得算法代碼條理不清楚,降低代碼的可讀性。一般嵌套分支結構不要超過三層,否則的話就要考慮調整算法,或者用函數封裝替換分支代碼,以提高算法代碼的可讀性。對于簡單條件的多層嵌套,可以使用組合條件來避免多層分支嵌套,比如前面的兩層嵌套結構就可以調整為:if(條件1&&條件2)

{

分支流程

}

當算法的某個步驟需要多重篩選條件時,就會用到多分支結構,多分支結構可以表示為:if(條件1)

{

分支流程1

}

elseif(條件2)

{

分支流程2

}

...

else

{

分支流程n

}

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

structActionProcess

{

Actionact;

ProcessNextFuncPtrprocessFunc;

};

然后分別為農夫的8個動作指定處理函數,得到函數表的定義: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結構,處理狀態轉換可能需要30多行代碼,而利用這個函數表,處理狀態轉換的代碼只有幾行:ItemStatenext;

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

{

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

{

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

break;

}

}

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

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

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

溫馨提示

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

評論

0/150

提交評論