转自把《编程珠玑》读薄
http://hawstein.com/posts/make-thiner-programming-pearls.html#Heap
開篇
具體化你的解決的問題。下面是A和B的對(duì)話。
A:我該如何對(duì)磁盤文件進(jìn)行排序? B:需要排序的內(nèi)容是什么?文件中有多少條記錄?每個(gè)記錄的格式是什么? A:該文件包含至多10,000,000個(gè)記錄,每條記錄都是一個(gè)7位整數(shù)。 B:如果文件那么小,為什么要使用磁盤排序呢?為什么不在主存中對(duì)它排序? A:該功能是某大型系統(tǒng)中的一部分,大概只能提供1MB主存給它。 B:你能將記錄方面的內(nèi)容說得更詳細(xì)一些嗎? A:每個(gè)記錄是一個(gè)7位正整數(shù),沒有其它的關(guān)聯(lián)數(shù)據(jù),每個(gè)整數(shù)至多只能出現(xiàn)一次。 ... ...經(jīng)過一系統(tǒng)的問題,我們可以將一個(gè)定義模糊不清的問題變得具體而清晰:
輸入: 所輸入的是一個(gè)文件,至多包含n個(gè)正整數(shù),每個(gè)正整數(shù)都要小于n,這里n=10^7。 如果輸入時(shí)某一個(gè)整數(shù)出現(xiàn)了兩次,就會(huì)產(chǎn)生一個(gè)致命的錯(cuò)誤。 這些整數(shù)與其它任何數(shù)據(jù)都不關(guān)聯(lián)。 輸出: 以增序形式輸出經(jīng)過排序的整數(shù)列表。 約束: 大概有1MB的可用主存,但可用磁盤空間充足。運(yùn)行時(shí)間至多允許幾分鐘, 10秒鐘是最適宜的運(yùn)行時(shí)間。如果主存容量不是嚴(yán)苛地限制在1MB,比如說可以是1MB多,或是1~2MB之間, 那么我們就可以一次性將所有數(shù)據(jù)都加載到主存中,用Bitmap來(lái)做。 10,000,000個(gè)數(shù)就需要10,000,000位,也就是10,000,000b = 1.25MB。
程序可分為三個(gè)部分:第一,初始化所有的位為0;第二,讀取文件中每個(gè)整數(shù), 如果該整數(shù)對(duì)應(yīng)的位已經(jīng)為1,說明前面已經(jīng)出現(xiàn)過這個(gè)整數(shù),拋出異常,退出程序 (輸入要求每個(gè)整數(shù)都只能出現(xiàn)一次)。否則,將相應(yīng)的位置1;第三, 檢查每個(gè)位,如果某個(gè)位是1,就寫出相應(yīng)的整數(shù),從而創(chuàng)建已排序的輸出文件。
如果主存容量嚴(yán)苛地限制在1MB,而使用Bitmap需要1.25MB, 因此無(wú)法一次載入完成排序。那么,我們可以將該文件分割成兩個(gè)文件, 再分別用Bitmap處理。分割策略可以簡(jiǎn)單地把前一半的數(shù)據(jù)放到一個(gè)文件, 后一半的數(shù)據(jù)放到另一個(gè)文件,分別排序后再做歸并。 也可以把文件中小于某個(gè)數(shù)(比如5,000,000)的整數(shù)放到一個(gè)文件,叫l(wèi)ess.txt, 把其余的整數(shù)放到另一個(gè)文件,叫g(shù)reater.txt。分別排序后, 把greater.txt的排序結(jié)果追加到less.txt的排序結(jié)果即可。
啊哈!算法
第2章圍繞3個(gè)問題展開。
- 給定一個(gè)包含32位整數(shù)的順序文件,它至多只能包含40億個(gè)這樣的整數(shù), 并且整數(shù)的次序是隨機(jī)的。請(qǐng)查找一個(gè)此文件中不存在的32位整數(shù)。 在有足夠主存的情況下,你會(huì)如何解決這個(gè)問題? 如果你可以使用若干外部臨時(shí)文件,但可用主存卻只有上百字節(jié), 你會(huì)如何解決這個(gè)問題?
這是CTCI中的一道題目,詳細(xì)解答請(qǐng)戳以下鏈接:
請(qǐng)猛戳我
- 請(qǐng)將一個(gè)具有n個(gè)元素的一維向量向左旋轉(zhuǎn)i個(gè)位置。例如,假設(shè)n=8,i=3, 那么向量abcdefgh旋轉(zhuǎn)之后得到向量defghabc。
這個(gè)問題很常見了,做3次翻轉(zhuǎn)即可,無(wú)需額外空間:
reverse(0, i-1); // cbadefgh reverse(i, n-1); // cbahgfed reverse(0, n-1); // defghabc- 給定一本英語(yǔ)單詞詞典,請(qǐng)找出所有的變位詞集。例如,因?yàn)椤皃ots”, “stop”,“tops”相互之間都是由另一個(gè)詞的各個(gè)字母改變序列而構(gòu)成的, 因此這些詞相互之間就是變位詞。
這個(gè)問題可以分3步來(lái)解決。第一步將每個(gè)單詞按字典序排序, 做為原單詞的簽名,這樣一來(lái),變位詞就會(huì)具有相同的簽名。 第二步對(duì)所有的單詞按照其簽名進(jìn)行排序,這樣一來(lái),變位詞就會(huì)聚集到一起。 第三步將變位詞分組,形成變位詞集。示意圖如下:
數(shù)據(jù)決定程序結(jié)構(gòu)
恰當(dāng)?shù)臄?shù)據(jù)視圖實(shí)際上決定了程序的結(jié)構(gòu)。 我們常常可以通過重新組織內(nèi)部數(shù)據(jù)來(lái)使程序變得小而美。
發(fā)明家悖論:更一般性的問題也許更容易解決。(有時(shí)候吧)
程序員在節(jié)省空間方面無(wú)計(jì)可施時(shí),將自己從代碼中解脫出來(lái), 退回起點(diǎn)并集中心力研究數(shù)據(jù),常常能有奇效。數(shù)據(jù)的表示形式是程序設(shè)計(jì)的根本。
下面是退回起點(diǎn)進(jìn)行思考時(shí)的幾條原則:
-
使用數(shù)組重新編寫重復(fù)代碼。冗長(zhǎng)的相似代碼常常可以使用最簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)—— 數(shù)組來(lái)更好地表述。
-
封裝復(fù)雜結(jié)構(gòu)。當(dāng)需要非常復(fù)雜的數(shù)據(jù)結(jié)構(gòu)時(shí),使用抽象術(shù)語(yǔ)進(jìn)行定義, 并將操作表示為類。
-
盡可能使用高級(jí)工具。超文本,名字-值對(duì),電子表格,數(shù)據(jù)庫(kù), 編程語(yǔ)言等都是特定問題領(lǐng)域中的強(qiáng)大的工具。
-
從數(shù)據(jù)得出程序的結(jié)構(gòu)。在動(dòng)手編寫代碼之前,優(yōu)秀的程序員會(huì)徹底理解輸入, 輸出和中間數(shù)據(jù)結(jié)構(gòu),并圍繞這些結(jié)構(gòu)創(chuàng)建程序。
提到的書籍:Polya的《How to Solve it》,中文書《怎樣解題》; Kernighan和Plauger的《Elements of Programming Style》;Fred Brooks的《人月神話》 Steve McConnell的《代碼大全》;《Rapid Development》; 《Software Project Survival Guide》
編寫正確的程序
本章以二分搜索為例子,講述了如何對(duì)程序進(jìn)行驗(yàn)證及正確性分析。
深入閱讀:David Gries的《Science of Programming》 是程序驗(yàn)證領(lǐng)域里極佳的一本入門書籍。
編程中的次要問題
到目前為止,你已經(jīng)做了一切該做的事:通過深入挖掘定義了正確的問題, 通過仔細(xì)選擇算法和數(shù)據(jù)結(jié)構(gòu)平衡了真正的需求,通過程序驗(yàn)證技術(shù)寫出了優(yōu)雅的代碼, 并且對(duì)其正確性相當(dāng)有把握。萬(wàn)事俱備,只欠編程。
-
使用斷言assert
-
自動(dòng)化測(cè)試程序
進(jìn)階閱讀:《Practice of Programming》第5章(調(diào)試),第6章(測(cè)試) 《Code Complete》第25章(單元測(cè)試),第26章(調(diào)試)
程序性能分析
下圖展示了一個(gè)程序的性能提升過程, 該程序的作用是對(duì)三維空間中n個(gè)物體的運(yùn)動(dòng)進(jìn)行仿真。從圖中可以看出, 一個(gè)程序可以從多方面進(jìn)行性能提升,而其中算法和數(shù)據(jù)結(jié)構(gòu)的選擇又顯得尤為重要。
從設(shè)計(jì)層面提升程序性能:
深入閱讀:Butler Lampson的“Hints for Computer System Design”, 該論文特別適合于集成硬件和軟件的計(jì)算機(jī)系統(tǒng)設(shè)計(jì)。
粗略估算
這一章講述了估算技術(shù),我認(rèn)為是相當(dāng)有用的一章。
文中先拋出一個(gè)問題:密西西比河一天流出多少水?如果讓你來(lái)回答, 你會(huì)怎么答,注意不能去Google哦。
作者是這么回答這個(gè)問題:假設(shè)河的出口大約有1英里寬和20英尺深(1/250英里), 而河水的流速是每小時(shí)5英里,也就是每天120英里。則可以計(jì)算出一天的流量:
1英里 * 1/250英里 * 120英里/天 約等于 1/2 英里^3/天上述算式非常簡(jiǎn)單,可是在看到這些文字之前,如果有人真的問你, 密西西比河一天流出多少水?你真的能答上來(lái)嗎?還是愣了一下后,擺擺手,說: 這我哪知道!
對(duì)于上面的問題,我們至少可以注意到以下兩點(diǎn):
你需要把問題轉(zhuǎn)換成一個(gè)可計(jì)算的具體模型。這一點(diǎn)往往不需要太擔(dān)心, 因?yàn)槲覀冏龅氖枪浪?#xff0c;所以可以忽視很多無(wú)關(guān)緊要的因素,可以去簡(jiǎn)化你的模型, 記住我們要的只是一個(gè)粗略計(jì)算的結(jié)果。比如對(duì)于上面的問題, 計(jì)算密西西比河一天流出多少水其實(shí)就是計(jì)算其一天的流量,利用中學(xué)所學(xué)知識(shí), 流量 = 截面積 x 流速,那我們就只需計(jì)算密西西比河的出水口的截面積和流速即可。 我們可以將出水口簡(jiǎn)化成一個(gè)矩形,因此就只需要知道出水口的寬和深即可。
你需要知道常識(shí)性的東西。上面我們已經(jīng)把問題轉(zhuǎn)換成了一個(gè)可計(jì)算的具體模型: 流量 = 出水口寬 x 出水口深 x 流速。接下來(lái)呢?你需要代入具體的數(shù)值去求得答案。 而這就需要你具備一些常識(shí)性的知識(shí)了。比如作者就估計(jì)了密西西比河的出口有1英里寬, 20英尺深(如果你估計(jì)只有幾十米寬,那就相差得太離譜了)。 這些常識(shí)性的知識(shí)比第1點(diǎn)更值得關(guān)注,因?yàn)槟銦o(wú)法給出一個(gè)靠譜的估算值往往是因?yàn)檫@點(diǎn)。
當(dāng)我們懂得如何把一個(gè)問題具體化定義出來(lái)并為其選用適當(dāng)?shù)哪P?#xff0c; 并且我們也積累了必要的常識(shí)性的知識(shí)后,回答那些初看起來(lái)無(wú)從下手的問題也就不難了。 這就是估算的力量。
以下是估算時(shí)的一些有用提示:
-
兩個(gè)答案比一個(gè)答案好。即鼓勵(lì)你從多個(gè)角度去對(duì)一個(gè)問題進(jìn)行估算, 如果從不同角度得到的答案差別都不大,說明這個(gè)估算值是比較靠譜的。
-
快速檢驗(yàn)。即量綱檢驗(yàn)。即等式兩邊最終的量綱要一致。 這一點(diǎn)在等式簡(jiǎn)單的時(shí)候相當(dāng)顯而易見。比如位移的單位是米,時(shí)間單位是秒, 速度單位是米/秒,那顯然我們應(yīng)該要用位移去除以時(shí)間來(lái)得到速度, 這樣才能保證它們單位的一致。你可能會(huì)說,我了個(gè)去,這種小學(xué)生都懂的事, 你好意思拿出來(lái)講。其實(shí)不然,當(dāng)你面對(duì)的是一個(gè)具有多個(gè)變量的復(fù)雜物理公式, 或者你提出某種物理假設(shè),正在考慮將其公式化,該方法可以切切實(shí)實(shí)地幫你做出檢驗(yàn)。
-
經(jīng)驗(yàn)法則。“72法則”:1.假設(shè)以年利率r%投資一筆錢y年,如果r*y = 72, 那么你的投資差不多會(huì)翻倍。2.如果一個(gè)盤子里的菌群以每小時(shí)3%的速率增長(zhǎng), 那么其數(shù)量每天(24小時(shí))都會(huì)翻倍。在誤差不超過千分之五的情況下, \pi秒就是一個(gè)納世紀(jì)。也就是說:
3.14秒 = 10-9?* 100年 = 10-7?年
也就是說,1年大概是3.14x107?秒。所以如果有人告訴你,一個(gè)程序運(yùn)行107?秒, 你應(yīng)該能很快反應(yīng)出,他說的其實(shí)是4個(gè)月。
- 實(shí)踐。與許多其他活動(dòng)一樣,估算技巧只能通過實(shí)踐來(lái)提高。
如果問題的規(guī)模太大,我們還可以通過求解它的小規(guī)模同質(zhì)問題來(lái)做估算。比如, 我們想測(cè)試某個(gè)程序運(yùn)行10億次需要多長(zhǎng)時(shí)間,如果你真去跑10億次, 說不定運(yùn)行幾個(gè)小時(shí)都沒結(jié)束,那不是很悲劇?我們可以運(yùn)行這個(gè)程序1萬(wàn)次或是10萬(wàn)次, 得出結(jié)果然后倍增它即可。當(dāng)然,這個(gè)結(jié)果未必是準(zhǔn)確的, 因?yàn)槟銢]法保證運(yùn)行時(shí)間是隨著運(yùn)行次數(shù)線性增加的。謹(jǐn)慎起見,我們可以運(yùn)行不同的次數(shù), 來(lái)觀察它的變化趨勢(shì)。比如運(yùn)行10次,100次,1000次,10000次等, 觀察它的運(yùn)行時(shí)間是否是線性增加的,或是一條二次曲線。
有時(shí)候,我們需要為估算的結(jié)果乘上一個(gè)安全系數(shù)。比如, 我們預(yù)估完成某項(xiàng)功能需要時(shí)間t,那根據(jù)以往經(jīng)驗(yàn),也許我們需要為這個(gè)值乘上2或4, 這樣也許才是一個(gè)靠譜的預(yù)估值。
Little定律:系統(tǒng)中物體的平均數(shù)量等于物體離開系統(tǒng)的平均速率和每個(gè)物體在系統(tǒng)中停留 的平均時(shí)間的乘積。(如果物體離開和進(jìn)入系統(tǒng)的總體出入流是平衡的, 那么離開速率也就是進(jìn)入速率)
舉個(gè)例子,比如你正在排除等待進(jìn)入一個(gè)火爆的夜總會(huì), 你可以通過估計(jì)人們進(jìn)入的速率來(lái)了解自己還要等待多長(zhǎng)時(shí)間。根據(jù)Little定律, 你可以推論:這個(gè)地方可以容納約60人,每個(gè)人在里面逗留時(shí)間大約是3小時(shí), 因此我們進(jìn)入夜總會(huì)的速率大概是每小時(shí)20人。現(xiàn)在隊(duì)伍中我們前面還有20人, 也就意味著我們還要等待大約一個(gè)小時(shí)。
深入閱讀:Darrell Huff的《How To Lie With Statistics》;關(guān)鍵詞: 費(fèi)米近似(Fermi estimate, Fermi problem)
算法設(shè)計(jì)技術(shù)
這一章就一個(gè)小問題研究了4種不同的算法,重點(diǎn)強(qiáng)調(diào)這些算法的設(shè)計(jì)技術(shù)。 研究的這個(gè)小問題是一個(gè)非常常見的面試題:子數(shù)組之和的最大值。 如果之前沒有聽過,建議Google之。
深入閱讀:Aho,Hopcroft和Ullman的《Data Structures and Algorithms》 Cormen,Leiserson,Rivest和Stein的《Introduction to Algorithms》
代碼調(diào)優(yōu)
前面各章討論了提高程序效率的高層次方法:問題定義,系統(tǒng)結(jié)構(gòu), 算法設(shè)計(jì)及數(shù)據(jù)結(jié)構(gòu)選擇。本章討論的則是低層次的方法:代碼調(diào)優(yōu)。
代碼調(diào)優(yōu)的最重要原理就是盡量少用它。不成熟的優(yōu)化是大量編程災(zāi)害的根源。 它會(huì)危及程序的正確性,功能性以及可維護(hù)性。當(dāng)效率很重要時(shí), 第一步就是對(duì)系統(tǒng)進(jìn)行性能監(jiān)視,以確定其運(yùn)行時(shí)間的分布狀況。 效率問題可以由多種方法來(lái)解決,只有在確信沒有更好的解決方案時(shí)才考慮進(jìn)行代碼調(diào)優(yōu)。
事實(shí)上,如果不是十分十分必要,不要去做代碼調(diào)優(yōu), 因?yàn)樗鼤?huì)犧牲掉軟件的其他許多性質(zhì)。
so,just skip this chapter。
節(jié)省空間
本章講述了節(jié)省空間的一些重要方法。
減少程序所需數(shù)據(jù)的存儲(chǔ)空間,一般有以下方法:
- 不存儲(chǔ),重新計(jì)算。
- 稀疏數(shù)據(jù)結(jié)構(gòu)。下面著重講一下這點(diǎn)。
- 數(shù)據(jù)壓縮。可以通過壓縮的方式對(duì)對(duì)象進(jìn)行編碼,以減少存儲(chǔ)空間。
- 分配策略。只有在需要的時(shí)候才進(jìn)行分配。
- 垃圾回收。對(duì)廢棄的存儲(chǔ)空間進(jìn)行回收再利用。
以下是節(jié)省代碼空間的幾種通用技術(shù):
- 函數(shù)定義。用函數(shù)替換代碼中的常見模式可以簡(jiǎn)化程序,同時(shí)減少代碼的空間需求。
- 解釋程序。用解釋程序命令替換長(zhǎng)的程序文本。
- 翻譯成機(jī)器語(yǔ)言。可以將大型系統(tǒng)中的關(guān)鍵部分用匯編語(yǔ)言進(jìn)行手工編碼。
稀疏數(shù)據(jù)結(jié)構(gòu)
假設(shè)我們有一個(gè)200 x 200的矩陣(共40000個(gè)元素),里面只有2000個(gè)元素有值, 其它的都為0,示意圖如下:
顯然這是一個(gè)稀疏矩陣,直接用一個(gè)200 x 200 的二維數(shù)組來(lái)存儲(chǔ)這些數(shù)據(jù)會(huì)造成大量的空間浪費(fèi),共需要200x200x4B=160KB。 所以,我們應(yīng)該想辦法用另一種形式來(lái)存儲(chǔ)這些數(shù)據(jù)。
方法一
使用數(shù)組表示所有的列,同時(shí)使用鏈表來(lái)表示給定列中的活躍元素。 如下圖所示:
該結(jié)構(gòu)中,有200個(gè)指針(colhead)和2000條記錄(每條記錄是兩個(gè)整數(shù)和一個(gè)指針), 占用空間是200x4B + 2000x12B = 24800B = 24.8KB, 比直接用二維數(shù)組存儲(chǔ)(160KB)要小很多。
方法二
我們可以開三個(gè)數(shù)組來(lái)保存這些數(shù),如下圖所示:
firstincol是一個(gè)長(zhǎng)度為201的數(shù)組,對(duì)于第i列,在數(shù)組row中, 下標(biāo)為firstincol[i]到firstincol[i+1]-1對(duì)應(yīng)的行元素非0, 其值存儲(chǔ)在相應(yīng)的pointnum數(shù)組中。
比如對(duì)于上圖,在第0列中,元素值非0的行有3行,分別是row[0],row[1],row[2], 元素值是pointnum[0],pointnum[1],pointnum[2];在第1列中,元素值非0的行有2行, 分別是row[3],row[4],元素值是pointnum[3],pointnum[4]。依次類推。
該結(jié)構(gòu)所需要的存儲(chǔ)空間為2x2000x4B + 201x4B = 16804B = 16.8KB。 由于row數(shù)組中的元素全部都小于200,所以每個(gè)元素可以用一個(gè)unsigned char來(lái)保存, firstincol數(shù)組中元素最大也就2000,所以可以用一個(gè)short(或unsigned short)來(lái)保存, pointnum中的元素是一個(gè)4B的int, 最終所需空間變?yōu)?#xff1a;2000x4B + 2000x1B + 201x2B = 10402B = 10.4KB。
深入閱讀:Fred Brooks的《人月神話》
排序
本章先簡(jiǎn)單介紹了插入排序,然后著重講述快速排序。
插入排序
// 版本1 void InsertSort(int a[], int n) { for(int i=1; i<n; ++i) for(int j=i; j>0 && a[j-1]>a[j]; --j) swap(a[j-1], a[j]); } // 版本2 void InsertSort1(int a[], int n) { for(int i=1; i<n; ++i) { int t = a[i]; int j = i; for(; j>0 && a[j-1]>t; --j) a[j] = a[j-1]; a[j] = t; } }快速排序
我們?cè)谶@里規(guī)定:小于等于pivot的元素移到左邊,大于pivot的元素移到右邊。
實(shí)現(xiàn)1:單向移動(dòng)版本
這個(gè)版本的關(guān)鍵是設(shè)置一快一慢兩個(gè)指針,慢指針左側(cè)都是小于等于pivot(包含慢指針?biāo)谖恢?, 慢指針到快指針之間的值是大于pivot,快指針右側(cè)的值是還未比較過的。示意圖如下:
小于等于pivot | 大于pivot | ?slow fast快指針一次一步向前走,遇到大于pivot什么也不做繼續(xù)向前走。遇到小于等于pivot的元素, 則慢指針slow向前走一步,然后交換快慢指針指向的元素。一次劃分結(jié)束后, 再遞歸對(duì)左右兩側(cè)的元素進(jìn)行快排。代碼如下:
// 數(shù)組快排 void QSort(int a[], int head, int end) { if(a==NULL || head==end) return; int slow = head, fast = head + 1; int pivot = a[head]; while(fast != end) { if(a[fast] <= pivot) swap(a[++slow], a[fast]); ++fast; } swap(a[head], a[slow]); QSort(a, head, slow); QSort(a, slow+1, end); }排序數(shù)組a只需要調(diào)用QSort(a, 0, n)即可。該思路同樣可以很容易地在鏈表上實(shí)現(xiàn):
// 單鏈表快排 void qsort(Node *head, Node *end){ if(head==NULL || head==end) return; Node *slow = head, *fast = head->next; int pivot = head->data; while(fast != end){ if(fast->data <= pivot){ slow = slow->next; swap(slow->data, fast->data); } fast = fast->next; } swap(head->data, slow->data); qsort(head, slow); qsort(slow->next, end); }排序頭指針為head的單鏈表只需調(diào)用qsort(head, NULL)即可。
實(shí)現(xiàn)2:雙向移動(dòng)版本
版本1能能夠快速完成對(duì)隨機(jī)整數(shù)數(shù)組的排序,但如果數(shù)組有序, 或是數(shù)組中元素相同,快排的時(shí)間復(fù)雜度會(huì)退化成O(n2?),性能變得非常差。
一種緩解方案是使用雙向移動(dòng)版本的快排,它每次劃分也是使用兩個(gè)指針, 不過一個(gè)是從左向右移動(dòng),一個(gè)是從右向左移動(dòng),示意圖如下:
小于等于pivot | ? | 大于pivoti j指針j不斷向左移動(dòng),直到遇到小于等于pivot,就交換指針i和j所指元素 (指針i一開始指向pivot);指針i不斷向右移動(dòng),直到遇到大于pivot的, 就交換指針i和j所指元素。pivot在這個(gè)過程中,不斷地?fù)Q來(lái)?yè)Q去, 最終會(huì)停在分界線上,分界線左邊都是小于等于它的元素,右邊都是大于它的元素。 這樣就避免了最后還要交換一次pivot的操作,代碼也變得美觀許多。
int partition(int a[], int low, int high){ int pivot = a[low], i=low, j=high; while(i < j){ while(i<j && a[j]>pivot) --j; if(i < j) swap(a[i], a[j]); while(i<j && a[i]<=pivot) ++i; if(i < j) swap(a[i], a[j]); } return i; } void quicksort(int a[], int first, int last){ if(first<last){ int k = partition(a, first, last); quicksort(a, first, k-1); quicksort(a, k+1, last); } }當(dāng)然,如果對(duì)于partition函數(shù),你如果覺得大循環(huán)內(nèi)的兩個(gè)swap還是做了些無(wú)用功的話, 也可以把pivot的賦值放到最后一步,而不是在這個(gè)過程中swap來(lái)swap去的。代碼如下:
int partition(int a[], int low, int high){ int pivot = a[low], i=low, j=high; while(i<j){ while(i<j && a[j]>pivot) --j; if(i<j) a[i++] = a[j]; while(i<j && a[i]<=pivot) ++i; if(i<j) a[j--] = a[i]; } a[i] = pivot; return i; }如果數(shù)組基本有序,那隨機(jī)選擇pivot(而不像上面那樣選擇第一個(gè)做為pivot) 會(huì)得到更好的性能。在partition函數(shù)里,我們只需要在數(shù)組中隨機(jī)選一個(gè)元素, 然后將它和數(shù)組中第一個(gè)元素交換,后面的劃分代碼無(wú)需改變, 就可以達(dá)到隨機(jī)選擇pivot的效果。
進(jìn)一步優(yōu)化
對(duì)于小數(shù)組,用插入排序之類的簡(jiǎn)單方法來(lái)排序反而會(huì)更快,因此在快排中, 當(dāng)數(shù)組長(zhǎng)度小于某個(gè)值時(shí),我們就什么也不做。對(duì)應(yīng)到代碼中, 就是修改quicksort中的if條件:
if(first < last) 改為 if(last-first > cutoff)其中cutoff是一個(gè)小整數(shù)。程序結(jié)束時(shí),數(shù)組并不是有序的, 而是被組合成一塊一塊隨機(jī)排列的值,并且滿足這樣的條件: 某一塊中的元素小于它右邊任何塊中的元素。我們必須通過另一種排序算法對(duì)塊內(nèi)進(jìn)行排序。 由于數(shù)組是幾乎有序的,因此插入排序比較適用。
這種方法結(jié)合了快排和插入排序,讓它們?nèi)プ龈髯陨瞄L(zhǎng)的事情,往往比單純用快排要快。
深入閱讀:Don Knuth的《The Art of Computer Programming, Volume 3: Sorting and Searching》;Robert Sedgewick的《Algorithms》; 《Algorithms in C》,《Algorithms in C++》,《Algorithms in Java》。
取樣問題
本章講述了一個(gè)小的隨機(jī)抽樣問題,并用不同的方法來(lái)解決它。
問題:對(duì)于整數(shù)m和n,其中m<n,輸出0~n-1范圍內(nèi)m個(gè)隨機(jī)整數(shù)的有序列表, 不允許重復(fù)。
比如m=3, n=5,那么一種可能輸出是0,2,3(要求有序)。實(shí)現(xiàn)1來(lái)自Knuth的TAOCP, 時(shí)間復(fù)雜度O(n):
void GenKnuth(int m, int n) { for(int i=0; i<n; ++i) { if((bigrand()%(n-i)) < m) { cout<<i<<endl; --m; } } }其中,bigrand()的作用是返回一個(gè)很大的隨機(jī)整數(shù)。
實(shí)現(xiàn)2:在一個(gè)初始為空的集合里面插入隨機(jī)整數(shù),直到個(gè)數(shù)足夠。代碼如下:
void GenSets(int m, int n) { set<int> s; while(s.size() < m) s.insert(bigrand() % n); set<int>::iterator i; for(i=s.begin(); i!=s.end(); ++i) cout<<*i<<endl; }實(shí)現(xiàn)3:把包含整數(shù)0~n-1的數(shù)組順序打亂,然后把前m個(gè)元素排序輸出。 該方法的性能通常不如Knuth的算法。代碼如下:
void GenShuf(int m, int n) { int x[n]; for(int i=0; i<n; ++i) x[i] = i; for(int i=0; i<m; ++i) { int j = randint(i, n-1); swap(x[i], x[j]); } sort(x, x+m); for(int i=0; i<m; ++i) cout<<x[i]<<endl; }深入閱讀:Don Knuth的《The Art of Computer Programming, Volume 2: Seminumerical Algorithms》
搜索
本章詳細(xì)研究這樣一個(gè)搜索問題:在沒有其他相關(guān)數(shù)據(jù)的情況下,如何存儲(chǔ)一組整數(shù)? 為些介紹了5種數(shù)據(jù)結(jié)構(gòu):有序數(shù)組,有序鏈表,二叉搜索樹,箱,位向量。
其中,二叉搜索樹應(yīng)該熟練掌握,以下是一種實(shí)現(xiàn):
struct Node {int data; Node *lchild, *rchild, *parent; Node(): lchild(NULL), rchild(NULL), parent(NULL) { } }; class BST { private: static const int kMax = 1000; Node *root_, *parent_, nodes_[kMax]; int size_; private: Node* minimum(Node* node); Node* maximum(Node* node); Node* successor(Node* node); Node* predecessor(Node* node); void Insert(Node* &node, int x); void InorderTraver(Node* node); Node* Find(Node* node, int x); public: BST(): root_(NULL), parent_(NULL), size_(0) { memset(nodes_, '\0', sizeof(nodes_)); } void Insert(int x); void InorderTraver(); Node* Find(int x); void Remove(Node* z); }; Node* BST::minimum(Node* node) { if(node == NULL) return NULL; while(node->lchild) node = node->lchild; return node; } Node* BST::maximum(Node* node) { if(node == NULL) return NULL; while(node->rchild) node = node->rchild; return node轉(zhuǎn)載于:https://www.cnblogs.com/heidsoft/p/4003220.html
總結(jié)
以上是生活随笔為你收集整理的转自把《编程珠玑》读薄的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HttpURLConnection与 H
- 下一篇: CodeForces 359D (数论+