⑴ 數據結構一道排序題怎麼排啊我想知道思路 答案已經有請告訴幫我分析一下
快速排序是對冒泡排序的一種改進。它的基本思想是:通過一躺排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一不部分的所有數據都要小,然後再按次方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
假設要排序的數組是A[1]……A[N],首先任意選取一個數據(通常選用第一個數據)作為關鍵數據,然後將所有比它的數都放到它前面,所有比它大的數都放到它後面,這個過程稱為一躺快速排序。一躺快速排序的演算法是:
1)、設置兩個變數I、J,排序開始的時候I:=1,J:=N;
2)以第一個數組元素作為關鍵數據,賦值給X,即X:=A[1];
3)、從J開始向前搜索,即由後開始向前搜索(J:=J-1),找到第一個小於X的值,兩者交換;
4)、從I開始向後搜索,即由前開始向後搜索(I:=I+1),找到第一個大於X的值,兩者交換;
5)、重復第3、4步,直到I=J;
例如:待排序的數組A的值分別是:(初始關鍵數據X:=49)
A[1] A[2] A[3] A[4] A[5] A[6] A[7]:
49 38 65 97 76 13 27
進行第一次交換後: 27 38 65 97 76 13 49
( 按照演算法的第三步從後面開始找
進行第二次交換後: 27 38 49 97 76 13 65
( 按照演算法的第四步從前面開始找>X的值,65>49,兩者交換,此時I:=3 )
進行第三次交換後: 27 38 13 97 76 49 65
( 按照演算法的第五步將又一次執行演算法的第三步從後開始找
進行第四次交換後: 27 38 13 49 76 97 65
( 按照演算法的第四步從前面開始找大於X的值,97>49,兩者交換,此時J:=4 )
此時再執行第三不的時候就發現I=J,從而結束一躺快速排序,那麼經過一躺快速排序之後的結果是:27 38 13 49 76 97 65,即所以大於49的數全部在49的後面,所以小於49的數全部在49的前面。
快速排序就是遞歸調用此過程——在以49為中點分割這個數據序列,分別對前面一部分和後面一部分進行類似的快速排序,從而完成全部數據序列的快速排序,最後把此數據序列變成一個有序的序列,根據這種思想對於上述數組A的快速排序的全過程如圖6所示:
初始狀態 {49 38 65 97 76 13 27}
進行一次快速排序之後劃分為 {27 38 13} 49 {76 97 65}
分別對前後兩部分進行快速排序 {13} 27 {38}
結束 結束 {49 65} 76 {97}
49 {65} 結束
結束
圖6 快速排序全過程
1)、設有N(假設N=10)個數,存放在S數組中;
2)、在S[1。。N]中任取一個元素作為比較基準,例如取T=S[1],起目的就是在定出T應在排序結果中的位置K,這個K的位置在:S[1。。K-1]<=S[K]<=S[K+1..N],即在S[K]以前的數都小於S[K],在S[K]以後的數都大於S[K];
3)、利用分治思想(即大化小的策略)可進一步對S[1。。K-1]和S[K+1。。N]兩組數據再進行快速排序直到分組對象只有一個數據為止。
如具體數據如下,那麼第一躺快速排序的過程是:
數組下標: 1 2 3 4 5 6 7 8 9 10
45 36 18 53 72 30 48 93 15 36
I J
(1) 36 36 18 53 72 30 48 93 15 45
(2) 36 36 18 45 72 30 48 93 15 53
(3) 36 36 18 15 72 30 48 93 45 53
(4) 36 36 18 15 45 30 48 93 72 53
(5) 36 36 18 15 30 45 48 93 72 53
通過一躺排序將45放到應該放的位置K,這里K=6,那麼再對S[1。。5]和S[6。。10]分別進行快速排序。
一般來說,冒泡法是程序員最先接觸的排序方法,它的優點是原理簡單,編程實現容易,但它的缺點就是--程序的大忌--速度太慢。下面我介紹一個理解上簡單但編程實現上不是太容易的排序方法,我不知道它是不是現有排序方法中最快的,但它是我見過的最快的。排序同樣的數組,它所需的時間只有冒泡法的 4% 左右。我暫時稱它為「快速排序法」。
「快速排序法」使用的是遞歸原理,下面我結合一個例子來說明「快速排序法」的原理。首先給出一個數組{53,12,98,63,18,72,80,46, 32,21},先找到第一個數--53,把它作為中間值,也就是說,要把53放在一個位置,使得它左邊的值比它小,右邊的值比它大。{21,12,32, 46,18,53,80,72,63,98},這樣一個數組的排序就變成了兩個小數組的排序--53左邊的數組和53右邊的數組,而這兩個數組繼續用同樣的方式繼續下去,一直到順序完全正確。
我這樣講你們是不是很胡塗,不要緊,我下面給出實現的兩個函數:
/*
n就是需要排序的數組,left和right是你需要排序的左界和右界,
如果要排序上面那個數組,那麼left和right分別是0和9
*/
void quicksort(int n[], int left,int right)
{
int dp;
if (left<right) {
/*
這就是下面要講到的函數,按照上面所說的,就是把所有小於53的數放
到它的左邊,大的放在右邊,然後返回53在整理過的數組中的位置。
*/
dp=partition(n,left,right);
quicksort(n,left,dp-1);
quicksort(n,dp+1,right); //這兩個就是遞歸調用,分別整理53左邊的數組和右邊的數組
}
}
我們上面提到先定位第一個數,然後整理這個數組,把比這個數小的放到它的左邊,大的放右邊,然後
返回這中間值的位置,下面這函數就是做這個的。
int partition(int n[],int left,int right)
{
int lo,hi,pivot,t;
pivot=n[left];
lo=left-1;
hi=right+1;
while(lo+1!=hi) {
if(n[lo+1]<=pivot)
lo++;
else if(n[hi-1]>pivot)
hi--;
else {
t=n[lo+1];
n[++lo]=n[hi-1];
n[--hi]=t;
}
}
n[left]=n[lo];
n[lo]=pivot;
return lo;
}
這段程序並不難,應該很好看懂,我把過程大致講一下,首先你的腦子里先浮現一個數組和三個指針,第一個指針稱為p指針,在整個過程結束之前它牢牢的指向第一個數,第二個指針和第三個指針分別為lo指針和hi指針,分別指向最左邊的值和最右邊的值。lo指針和hi指針從兩邊同時向中間逼近,在逼近的過程中不停的與p指針的值比較,如果lo指針的值比p指針的值小,lo++,還小還++,再小再++,直到碰到一個大於p指針的值,這時視線轉移到hi指針,如果 hi指針的值比p指針的值大,hi--,還大還--,再大再--,直到碰到一個小於p指針的值。這時就把lo指針的值和hi指針的值做一個調換。持續這過程直到兩個指針碰面,這時把p指針的值和碰面的值做一個調換,然後返回p指針新的位置。
⑵ C語言快速排序演算法問題
快速排序法」使用的是遞歸原理,下面我結合一個例子來說明「快速排序法」的原理。首先給出一個數組{53,12,98,63,18,72,80,46, 32,21},先找到第一個數--53,把它作為中間值,也就是說,要把53放在一個位置,使得它左邊的值比它小,右邊的值比它大。{21,12,32, 46,18,53,80,72,63,98},這樣一個數組的排序就變成了兩個小數組的排序--53左邊的數組和53右邊的數組,而這兩個數組繼續用同樣的方式繼續下去,一直到順序完全正確。
一般來說,冒泡法是程序員最先接觸的排序方法,它的優點是原理簡單,編程實現容易,但它的缺點就是--程序的大忌--速度太慢。
附上快速排序代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<stdio.h>
void quicksort(int a[],int left,int right)
{
int i,j,temp;
i=left;
j=right;
temp=a[left];
if(left>right)
return;
while(i!=j)
{
while(a[j]>=temp&&j>i)
j--;
if(j>i)
a[i++]=a[j];
while(a[i]<=temp&&j>i)
i++;
if(j>i)
a[j--]=a[i];
}
a[i]=temp;
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
void main()
{
int a[]={53,12,98,63,18,72,80,46,32,21};
int i;
quicksort(a,0,9);
/*排好序的結果*/
for(i=0;i<10;i++)
printf("%4d\n",a[i]);
}
⑶ 一個優秀的程序員應該學完哪些計算機理論的知識
1、循環隊列:將隊列存儲空間的最後一個位置繞到第一個位置,形成邏輯上的環狀空間。
2、入隊運算:在循環隊列的隊尾加入一個新元素。上溢:當循環隊列非空,且隊尾指針等於隊頭指針時,說明循環隊列已滿,不能進行入隊運算。
3、退隊運算:在循環隊列的隊頭位置退出一個元素並賦給指定的變數。首先將隊頭指針進一,然後將排頭指針指向的元素賦給指定的變數。下溢:當循環隊列為空時,不能進行退隊運算。
⑷ 快速排序mpi演算法進程為2時為什麼加速不明顯
2012年偶決定開始寫博客了,不為別的,就希望可以通過博客記錄我的成長歷程同時也希望可以幫助一些剛畢業,剛入行業的兄弟姐們們。我們是一群充滿浮躁、抱怨、迷茫的程序猿,想一想3年就這么過去了,社會變得更浮躁了,鴨梨也越來越大,房?車?女人?… 抓狂… 決定寫這樣一篇文章,再次明確自己的職業規劃,也送給浮躁的你,踏上程序猿這條路可謂是路途艱辛,乏味,枯燥,在這里把自己的學習心得,目前正在計劃的規劃,列出來。希望對你有所幫助。 1、多動手,多思考 不要怕做不好,剛畢業或者剛入行最缺的就是工作經驗,沒有別的途徑,只有做,做,做,多做經驗就來了。項目做多了自然而然你就有了多種不同項目的業務邏輯,這些可是在大學根本無法學到的東西,也是企業最需要的。 任何一個企業需要的是一個快速上手,馬上解決業務任務的員工。面對現在層出不窮的新技術,各種復雜的業務邏輯需求,你是如何去應對呢?很簡單:做,做,做,我的學習過程就一個字:做! 截止目前從0 到 1 真的很辛苦,大大小小做了上百個(包括接的大大小小單子需求,之前公司各種業務需求解決),總結就一句話:做的多了 溝通就多了,溝通多了就更好的理解客戶需求和用戶展示的一些基本經驗。 2、少抱怨,多學習 當你剛進公司時,難免會做很多雜事,這個是無法避免。我剛進這個行業做了大半年的雜事,什麼DIV,CSS,JS 調個模板 改個小東西,等等。一切都是熬出來的,只有當你的經驗,技術積累到了足夠 應付業務需求的時候,你自然會被賞識,當然如果還是無法賞識,建議你可以立馬跳槽。對於程序員來說偶爾的跳槽也是成長的一部分,不知道對被人是否是這樣,但對我來說我的每次跳槽都會 一次快速成長。 作為程序猿我覺得如果真想做好,必須要有興趣,寫代碼會讓你瘋狂,讓你瘋癲,這樣你才能寫好它。如果你只是為了工作而去做,那麼你只是一個工具而非程序猿,那麼你也就 不要過於頻繁跳槽,因為每次跳槽對你來說都是一個大的挑戰。有了興趣,支持以恆,不要在乎任務多,壓力大,能做就做,你的目的就是提高自己,讓自己在明天比昨天更有價值,當價值積累到了足夠 的資本,很多公司會搶著要你,相信這個社會肯定會有識才的公司。耐心+機遇+堅定不移的信念,這就是我!一個簡單的程序猿。 3、制定有效的學習計劃 當你制定了計劃,就一定要堅持的完成,如果不能,請你不要制定計劃,因為這樣會讓你很痛苦。在公司或者各類技術社區盡量多認識大牛,多加群,多交流、討論,多幫助別人。 在制定計劃時,不要過於盲目。根據自身情況制定各個階段計劃,最好的計劃是短期計劃並且可以保證順利完成的計劃,如果制定一個飄渺無期的計劃,你更適合當一個演說家。 請不要吝嗇,在你計劃學習的過程中盡量寫到博客,比如CSDN博客 就是很不錯的選擇。這樣你即可以幫助新手,也可以讓自己更深入的熟悉學習的知識,並在需要是最快的速度查找到。 廢話基本說完了,下面介紹下我近期整理的計劃和一些學習心得,希望我們可以一起進步,一起提升,一起為了明天的更多薪資而努力。 PHP程序員突破成長瓶頸?(整理於網上,並自己也在逐條實施中) 對PHP的掌握不精(很多PHP手冊都沒有看完,庫除外) 知識面比較窄(面對需求,除開使用PHP和mysql ,不知道其它的解決辦法) PHP代碼以過程為主,認為面向對象的實現太繞,看不懂 這些PHPer 在遇到需要高性能,處理高並發,大量數據的項目或業務邏輯比較復雜(系統需要解決多領域業務的問題)時,缺少思路。不能分析問題的本質,技術判斷力比較差,對於問題較快能找出臨時的解決辦法,但常常在不斷臨時性的解決辦法中,系統和自己一步步走向崩潰。那怎麼提高自己呢?怎麼可以挑戰難度更高的系統? 高性能系統的挑戰在哪裡? 如何選擇WEB伺服器?要不要使用fast-cgi 模式 要不要使用反向代理服務?選擇全內存緩存還是硬碟緩存? 是否需要負載均衡?是基於應用層,還是網路層? 如何保證高可靠性? 你的PHP代碼性能如何,使用優化工具後怎麼樣? 性能瓶頸在那裡? 是否需要寫成C的擴展? 用戶訪問有什麼特點,是讀多還是寫多?是否需要讀寫分離? 數據如何存儲?寫入速度和讀出速度如何? 數據增漲訪問速讀如何變化? 如何使用緩存? 怎麼樣考慮失效?數據的一致性怎麼保證? 高復雜性系統的挑戰在哪裡? 能否識別業務所對應的領域?是一個還是多個? 能否合理對業務進行抽象,在業務規則變化能以很小的代價實現? 數據的一致性、安全性可否保證? 是否撐握了面向對象的分析和設計的方法 當我所列出的問題,你都能肯定的回答,我想在技術上你基本已經可能成為架構師了。 怎麼樣提高,突破瓶頸? 分析你所使用的技術其原理和背後運行的機制,這樣可以提高你的技術判斷力,提高你技術方案選擇的正確性; 學習大學期間重要的知識, 操作系統原理,數據結構和演算法。知道你以前學習都是為了考試,但現在你需要為自己學習,讓自己知其所以然。 重新開始學習C語言,雖然你在大學已經學過。這不僅是因為你可能需要寫PHP擴展,而且還因為,在做C的應用中,有一個時刻關心性能、內存控制、變數生命周期、數據結構和演算法的環境。 學習面向對象的分析與設計,它是解決復雜問題的有效的方法。學習抽象,它是解決復雜問題的唯一之道。 “這么多的東西怎麼學,這得學多久呀” ? 如果你努力的話,有較好的規劃,估計需要1~2年的時間,怎麼學習的問題,我們後續再談。 PHP學習的過程網上已經有很多教程了,就不列舉了。基礎+數據結構+演算法(PHP這個比較弱) 不斷重復的學習使用 其次是設計模式,尤其復雜的業務需求設計模式非常有幫助,我博客也會不斷地學習,講解各種設計模式的用法。 積累:把常用的一些庫(用過的,自己寫的)都收集起來,擋在用到時拿出來用即可,非常方便。如:分頁,圖片處理,上傳,下載,EMAIL等等這些常用到的。 多方位動手:不光要寫代碼,把代碼片段分析放到博客 也是進步提升的一個重要的環節,加深記憶不錯的方法。 1.PHP基礎入門(語法,常用函數和擴展) 2.面向對象的PHP(書籍:《深入PHP,面向對象、模式與實踐》) 3.網站軟體架構設計(設計模式、框架等) 4.網站物理層次架構設計(分布式計算、存儲、負載均衡、高可用性等) 引用:一個不錯的網上找到的學習建議 如何有效的學習是一個大問題。 自己有些實踐但很零散,不好總結。昨天晚上睡覺前,突然想到了RUP的核心,“以架構為中心,用例驅動,迭代開發”,借用這個思想,關於有效的學習的方法,可以這樣來表述: 以原理、模型或機制為中心,任務驅動,迭代學習 目的:學習如何提高處理性能。 可迭代驅動的任務:通過IP找到所在地域。 這是WEB應用常見的任務,IP資料庫是10左右萬行的記錄。 第一次迭代:不考慮性能的情況下實現功能(通過PHP來實現) 因為無法直接通過KEY(IP)進行查找地域,所以直接放到數據或通過關聯數組這種簡單的方法都是不行的。思路還是先把數據進行排序,然後再進行查找 1. 如何通過IP查找? 已序的數據,二分查找是最快的。 2. 如何排序?用庫函數sort當然 是可以,但是即然是學習,那還是自己實現快速排序吧。 學習目標:排序演算法,查找演算法 PHPer 數據結構和演算法基礎比較差,平時也沒有這方面的任務,自己也不學習,因此這方面的知識很缺乏。 但是,編程解決的問題,最終都會歸結到數據結構和對這種數據結構操作的演算法。 如果數據結構演算法常在心中,那遇到問題就能清晰認識到它內在的結構,解決方法就會自然產生。 第二次迭代:優化數據的載入與排序 如果做到第一步,那基本上還是不可用,因為數據每次都需要的載入和排序,這樣太耗時間。 解決的思路是,數據一次載入排序後,放到每個PHP進程能訪問到的地方 放到memcache 這是大家容易想到問題。 其實放到共享內存(EA等加速器都支持)中是更快的方式,因為memcache還多了網路操作。 數據是整體放入到共享內存,還是分塊放入,如何測試性能? 如何分析瓶頸所在(xdebug)? 在這些問題的驅動下你會學習到 學習目標:檢測、定位、優化PHP性能的方法; PHP實現結構對性能的影響。 第三次迭代:編寫PHP的擴展 怎麼確定需要學習的機制和原理呢? 怎麼找到驅動學習任務呢? 從這個技術的定位來找出需要學習的重點,即它怎麼做到(機制)的和它為什麼能這樣做到 (模型或原理) 列出這個技術最常見的應用,做為學習的任務,從簡到難進行實踐。 如果完全自學,找到需要學習的要點(機制、模型、原理) 設定學習任務的確不是那麼容易把握。如果找到一個有經驗的人來指導你或加一個學習型的團隊,那學習的速度的確會大大提高。 最後簡單總結下: 1、一定要有耐心,制定好計劃一定要實施 2、PHP基礎比較吃透,手動多了自然就會記得更深(PHP手冊一定要多次反復的閱讀) 3、PHP設計模式並在實際場景中嘗試應用,不斷地加強記憶和理解設計模式 4、現在新東西真的太快,所以為了適應就必須要多下功夫。內存緩存,文件緩存,靜態緩存,高並發處理,這些必須要熟練應用。 5、加強計算機系統原理的了解,熟悉常用數學知識,練習演算法應用。計算機科學本質上講是數學的一個學科。好的數學家中間會產出優秀的程序員。不要讓你的數學能力喪失殆盡。 邏輯學、離散數學、微積分、概率論、統計學、抽象代數、數論、范疇論、偏序理論這些數學知識盡量多練習,多熟悉下。 6、關注PHP安全,了解最新PHP,MYSQL版本更新和BUG動態。 7、深入學習數據結構和演算法,不論是什麼語言 最核心的就是數據結構和演算法。 8、開始學習C,或者同步和PHP進行也可以,看你的時間和學習強度計劃了。C是必須要學,如果你想走程序猿這條道路的話。PHP也是C寫的,而且 PHP運行機制也是通過編譯器編譯成C在電腦上運行,所以C學好了對你的開發之路只有益處。你的money 也會 賺的比以前 更多! 最後:認真做好每一項,學扎實,重復的去學。不知不覺中你的能力會得到很快的提升。 最後想說,沒什麼可以煩心的,不是所有人都是千里馬,也不是所有人都是伯樂。如果你是千里馬你可以慢慢尋找能賞識你的伯樂,如果你是伯樂也希望你早點找到屬於你的千里馬。 文章來源:劉昕明的博客
⑸ 誰能幫我講解一下 作業排序的一個更快演算法的思想~
快速排序是一種分割處理式的排序演算法,它將一個復雜的排序問題分解為若干較容易處理的排序問題,然後逐一解決。在快速排序演算法中,首先要從數據集的數據中選擇一個數據作為分割值,然後將數據分成以下3個子集:
(1) 將大於分割值的數據移到分割值前面,組成子集1;
(2) 分割值本身為子集2;
(3) 將小於分割值的數據移到分割值後面,組成子集3。
等於分割值的數據可以放在任意一個子集中,這對快速排序演算法沒有任何影響。由於子集2已經是有序的,所以此後只需對子集1和子集3進行快速排序。
需要注意的是,當數據集很小時,無法進行快速排序,而要使用其它排序演算法。顯然,當數據集中的數據只有兩個或更少時,就不可能將數據集再分割成三個子集。實際上,當數據集比較小時,程序員就應該考慮是否仍然採用快速排序演算法,因為在這種情況下另外一些排序演算法往往更快。
⑹ 作為程序員提高編程能力的幾個基礎演算法
一:快速排序演算法
快速排序是由東尼·霍爾所發展的一種排序演算法。在平均狀況下,排序n個項目要Ο(nlogn)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(nlogn)演算法更快,因為它的內部循環(innerloop)可以在大部分的架構上很有效率地被實現出來。
快速排序使用分治法(Divideandconquer)策略來把一個串列(list)分為兩個子串列(sub-lists)。
演算法步驟:
1從數列中挑出一個元素,稱為「基準」(pivot),
2重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱為分區(partition)操作。
3遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,但是這個演算法總會退出,因為在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。
二:堆排序演算法
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
堆排序的平均時間復雜度為Ο(nlogn) 。
創建一個堆H[0..n-1]
把堆首(最大值)和堆尾互換
3.把堆的尺寸縮小1,並調用shift_down(0),目的是把新的數組頂端數據調整到相應位置
4.重復步驟2,直到堆的尺寸為1
三:歸並排序
歸並排序(Mergesort,台灣譯作:合並排序)是建立在歸並操作上的一種有效的排序演算法。該演算法是採用分治法(DivideandConquer)的一個非常典型的應用。
1.申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合並後的序列
2.設定兩個指針,最初位置分別為兩個已經排序序列的起始位置
3.比較兩個指針所指向的元素,選擇相對小的元素放入到合並空間,並移動指針到下一位置
4.重復步驟3直到某一指針達到序列尾
5.將另一序列剩下的所有元素直接復制到合並序列尾
四:二分查找演算法
二分查找演算法是一種在有序數組中查找某一特定元素的搜索演算法。搜素過程從數組的中間元素開始,如果中間元素正好是要查找的元素,則搜素過程結束;如果某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,而且跟開始一樣從中間元素開始比較。如果在某一步驟數組為空,則代表找不到。這種搜索演算法每一次比較都使搜索范圍縮小一半。折半搜索每次把搜索區域減少一半,時間復雜度為Ο(logn) 。
五:BFPRT(線性查找演算法)
BFPRT演算法解決的問題十分經典,即從某n個元素的序列中選出第k大(第k小)的元素,通過巧妙的分析,BFPRT可以保證在最壞情況下仍為線性時間復雜度。該演算法的思想與快速排序思想相似,當然,為使得演算法在最壞情況下,依然能達到o(n)的時間復雜度,五位演算法作者做了精妙的處理。
1.將n個元素每5個一組,分成n/5(上界)組。
2.取出每一組的中位數,任意排序方法,比如插入排序。
3.遞歸的調用selection演算法查找上一步中所有中位數的中位數,設為x,偶數個中位數的情況下設定為選取中間小的一個。
4.用x來分割數組,設小於等於x的個數為k,大於x的個數即為n-k。
5.若i==k,返回x;若i<k,在小於x的元素中遞歸查找第i小的元素;若i>k,在大於x的元素中遞歸查找第i-k小的元素。
終止條件:n=1時,返回的即是i小元素。
六:DFS(深度優先搜索)
深度優先搜索演算法(Depth-First-Search),是搜索演算法的一種。它沿著樹的深度遍歷樹的節點,盡可能深的搜索樹的分支。當節點v的所有邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。這一過程一直進行到已發現從源節點可達的所有節點為止。如果還存在未被發現的節點,則選擇其中一個作為源節點並重復以上過程,整個進程反復進行直到所有節點都被訪問為止。DFS屬於盲目搜索。
深度優先搜索是圖論中的經典演算法,利用深度優先搜索演算法可以產生目標圖的相應拓撲排序表,利用拓撲排序表可以方便的解決很多相關的圖論問題,如最大路徑問題等等。一般用堆數據結構來輔助實現DFS演算法。
深度優先遍歷圖演算法步驟:
1.訪問頂點v;
2.依次從v的未被訪問的鄰接點出發,對圖進行深度優先遍歷;直至圖中和v有路徑相通的頂點都被訪問;
3.若此時圖中尚有頂點未被訪問,則從一個未被訪問的頂點出發,重新進行深度優先遍歷,直到圖中所有頂點均被訪問過為止。
上述描述可能比較抽象,舉個實例:
DFS在訪問圖中某一起始頂點v後,由v出發,訪問它的任一鄰接頂點w1;再從w1出發,訪問與w1鄰接但還沒有訪問過的頂點w2;然後再從w2出發,進行類似的訪問,…如此進行下去,直至到達所有的鄰接頂點都被訪問過的頂點u為止。
接著,退回一步,退到前一次剛訪問過的頂點,看是否還有其它沒有被訪問的鄰接頂點。如果有,則訪問此頂點,之後再從此頂點出發,進行與前述類似的訪問;如果沒有,就再退回一步進行搜索。重復上述過程,直到連通圖中所有頂點都被訪問過為止。
七:BFS(廣度優先搜索)
廣度優先搜索演算法(Breadth-First-Search),是一種圖形搜索演算法。簡單的說,BFS是從根節點開始,沿著樹(圖)的寬度遍歷樹(圖)的節點。如果所有節點均被訪問,則演算法中止。
BFS同樣屬於盲目搜索。一般用隊列數據結構來輔助實現BFS演算法。
1.首先將根節點放入隊列中。
2.從隊列中取出第一個節點,並檢驗它是否為目標。
如果找到目標,則結束搜尋並回傳結果。
否則將它所有尚未檢驗過的直接子節點加入隊列中。
3.若隊列為空,表示整張圖都檢查過了——亦即圖中沒有欲搜尋的目標。結束搜尋並回傳「找不到目標」。
4.重復步驟2。
八:Dijkstra演算法
戴克斯特拉演算法(Dijkstra』salgorithm)是由荷蘭計算機科學家艾茲赫爾·戴克斯特拉提出。迪科斯徹演算法使用了廣度優先搜索解決非負權有向圖的單源最短路徑問題,演算法最終得到一個最短路徑樹。該演算法常用於路由演算法或者作為其他圖演算法的一個子模塊。
該演算法的輸入包含了一個有權重的有向圖G,以及G中的一個來源頂點S。我們以V表示G中所有頂點的集合。每一個圖中的邊,都是兩個頂點所形成的有序元素對。(u,v)表示從頂點u到v有路徑相連。我們以E表示G中所有邊的集合,而邊的權重則由權重函數w:E→[0,∞]定義。因此,w(u,v)就是從頂點u到頂點v的非負權重(weight)。邊的權重可以想像成兩個頂點之間的距離。任兩點間路徑的權重,就是該路徑上所有邊的權重總和。已知有V中有頂點s及t,Dijkstra演算法可以找到s到t的最低權重路徑(例如,最短路徑)。這個演算法也可以在一個圖中,找到從一個頂點s到任何其他頂點的最短路徑。對於不含負權的有向圖,Dijkstra演算法是目前已知的最快的單源最短路徑演算法。
1.初始時令S=,T=,T中頂點對應的距離值
若存在<V0,Vi>,d(V0,Vi)為<V0,Vi>弧上的權值
若不存在<V0,Vi>,d(V0,Vi)為∞
2.從T中選取一個其距離值為最小的頂點W且不在S中,加入S
3.對其餘T中頂點的距離值進行修改:若加進W作中間頂點,從V0到Vi的距離值縮短,則修改此距離值
重復上述步驟2、3,直到S中包含所有頂點,即W=Vi為止
九:動態規劃演算法
動態規劃(Dynamicprogramming)是一種在數學、計算機科學和經濟學中使用的,通過把原問題分解為相對簡單的子問題的方式求解復雜問題的方法。動態規劃常常適用於有重疊子問題和最優子結構性質的問題,動態規劃方法所耗時間往往遠少於樸素解法。
動態規劃背後的基本思想非常簡單。大致上,若要解一個給定問題,我們需要解其不同部分(即子問題),再合並子問題的解以得出原問題的解。通常許多子問題非常相似,為此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量:一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。這種做法在重復子問題的數目關於輸入的規模呈指數增長時特別有用。
關於動態規劃最經典的問題當屬背包問題。
1.最優子結構性質。如果問題的最優解所包含的子問題的解也是最優的,我們就稱該問題具有最優子結構性質(即滿足最優化原理)。最優子結構性質為動態規劃演算法解決問題提供了重要線索。
2.子問題重疊性質。子問題重疊性質是指在用遞歸演算法自頂向下對問題進行求解時,每次產生的子問題並不總是新問題,有些子問題會被重復計算多次。動態規劃演算法正是利用了這種子問題的重疊性質,對每一個子問題只計算一次,然後將其計算結果保存在一個表格中,當再次需要計算已經計算過的子問題時,只是在表格中簡單地查看一下結果,從而獲得較高的效率。
十:樸素貝葉斯分類演算法
樸素貝葉斯分類演算法是一種基於貝葉斯定理的簡單概率分類演算法。貝葉斯分類的基礎是概率推理,就是在各種條件的存在不確定,僅知其出現概率的情況下,如何完成推理和決策任務。概率推理是與確定性推理相對應的。而樸素貝葉斯分類器是基於獨立假設的,即假設樣本每個特徵與其他特徵都不相關。
樸素貝葉斯分類器依靠精確的自然概率模型,在有監督學習的樣本集中能獲取得非常好的分類效果。在許多實際應用中,樸素貝葉斯模型參數估計使用最大似然估計方法,換言樸素貝葉斯模型能工作並沒有用到貝葉斯概率或者任何貝葉斯模型。
盡管是帶著這些樸素思想和過於簡單化的假設,但樸素貝葉斯分類器在很多復雜的現實情形中仍能夠取得相當好的效果。
通過掌握以上演算法,能夠幫你迅速提高編程能力,成為一名優秀的程序員。
⑺ 求編程領域上一些經典演算法同時也是程序員必須掌握的演算法
這是我在一個論壇里看到的,你也參考參考吧。C++的虛函數
======================
C++使用虛函數實現了其對象的多態,C++對象的開始四個位元組是指向虛函數表的指針,其初始化順序是先基類後派生類,所以該虛函數表永遠指向最後一個派生類,從而實現了相同函數在不同對象中的不同行為,使得對象既有共性,又有其個性。
內存池分配、回收之夥伴演算法
=======================
夥伴演算法是空閑鏈表法的一個增強演算法,依次建立2^0\2^1\2^2\2^3...2^n大小的 內存塊空閑鏈表,利用相鄰內存塊的夥伴性質,很容易將互為夥伴的內存塊進行合並移到相應的空閑鏈表或將一塊內存拆分成兩塊夥伴內存,一塊分配出去,另一塊掛入相應空閑鏈表,使得內存的分配和回收變得高效。
AVL樹
=======================
AVL樹是一個平衡二叉樹,其中序遍歷是從小到大排序的,該結構插入節點和檢索非常高效,被廣泛應用
快速排序
=======================
通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。效率非常高
密碼學之非對稱加密協議(公鑰、私鑰加密協議)
======================
非對稱加密演算法需要兩個密鑰,用其中一個加密產生的密文,只能通過另外一個密鑰解密,密鑰持有者A可以將其中一個公開,稱為公用密鑰,另外一個秘密保存稱為私鑰,這樣當某人B想給A傳一封秘信時,只要將密信使用A的公鑰加密後,就可以放心使用各種信道將迷信傳給A了,因為該密信只有A可以解密,第三者截取因為無法解密而毫無意義。
該演算法很好地解決了密鑰的安全傳遞的問題,因為公鑰和加密演算法都是公開的,私鑰不需要傳輸。
密碼學之數字簽名協議(身份鑒別、防抵賴)
======================
數字簽名也是建立在非對稱加密基礎之上的,如果A君用它的私鑰將文件加密後在發布,A君就無法抵賴該文件是其發布的,因為其他人能通過A君的公鑰將文件解密就說明,如果演算法可靠,該文件一定是A君用其私鑰加密的。
由於非對稱加密演算法的加密和解密很慢,現在的數字簽名並非是將其要發布的信息用其私鑰加密,而是先用一個單項散列演算法如(MD5)產生一個該信息的比較短的指紋(hash值),對其指紋用其私鑰加密後和信息一並發布,同樣達到了防抵賴的作用。
無回溯字元串模式匹配-kmp演算法
======================
他是根據子串的特徵,當匹配失敗時,不需要回溯,而是直接將字串向後滑動若干個位元組,繼續匹配,極大提高了匹配速度。該演算法被廣泛使用。詳細請參考數據結構教程。
最小路徑選路-迪傑斯特拉演算法、弗洛伊德演算法
======================
學習數據結構的時候,印象最深的就要算kmp演算法和最小路徑演算法了,因為理解他們比較費腦子,我是不可能發明這些演算法了,發明他們的都是天才,呵呵。
使用最短路徑的演算法曾經幫人寫過一個小東西,還是很有效的,記得是使用的弗洛伊德演算法的一個變種,要詳細了解的朋友可以查找相關資料,想將他們使用在你的項目中,代碼直接從教科書上抄就可以了,不需要理解。
tcp協議之-nagle演算法
======================
tcp、ip中令人叫絕的想法很多,印象最深的要算nagle演算法了。
tcp出於效率和流量控制的考慮,發送端的數據不是產生多少就馬上發送多少,一般是等到數據集聚到發送緩沖區長度的一半或者數據達到最大tcp數據包數據部分長度(好像是65515)才啟動發送,而且還要看接受端可用緩沖區的大小,如果接受端產生一個回應報文通知發送端沒有接受空間了,發送端哪怕緩沖區已經滿了,也不會啟動發送,直到接受端通告發送端其已經有了接受數據的空間了。
這樣就有一個問題,假如發送端就是要發送一個小報文(比如10個位元組),然後等待對方的回應。按照上面的方案,tcp會一直等數據收集到一定量才發送,於是矛盾就產生了。應用層不再發數據,tcp等不到足夠的數據不會將10個字的數據發送到網卡,接收端應用層收不到數據就不會回應發送端。
你也可能說,可以讓修改發送端發送條件,不一定要等到足夠的數據再發送,為了效率考慮,可以考慮延時一定的時間,比如說1秒,如果上層還沒有數據到來,就將發送緩沖中的數據發出去。當然這樣也是可行的,盡管應用端白白等了1秒鍾啥也沒干,呵呵。
其實nagle演算法很好解決了該問題,它的做發是鏈接建立後的第一次發送不用等待,直接將數據組裝成tcp報文發送出去,以後要麼等到數據量足夠多、要麼是等到接受方的確認報文,演算法及其簡單,而且很好解決了上面的矛盾。
socket之io模型設計
======================
windows下socket有兩種工作方式:
1)同步方式
2)非同步方式
同步socket又有兩種工作模式:
1)阻塞模式
2)非阻塞模式
阻塞模式是最簡單的工作模式,以tcp的發送數據為例,如果發送緩沖區沒有空間,send調用就不會返回,一直要等到能夠發出一點數據為止,哪怕是一個位元組,但是send返回並不表示我要發送的數據已經全部提交給了tcp,所以send返回時要檢查這次發送的數量,調整發送緩沖指針,繼續發送,直到所有數據都提交給了系統。
由於其阻塞的特性,會阻塞發送線程,所以單線程的程序是不適合使用阻塞模式通信的,一般使用一個連接一個線程的方法,但是這種方式對於要維護多個連接的程序,是個不好的選擇,線程越多,開銷越大。
同步非阻塞模式的socket不會阻塞通信線程,如果發送緩沖區滿,send調用也是立刻返回,接受緩沖區空,recv也不會阻塞,所以通信線程要反復調用send或recv嘗試發送或接收數據,對cpu是很大的浪費。
針對非阻塞的尷尬,介面開發人員發明了三種io模型來解決該問題:
1)選擇模型(select)
2)非同步選擇模型(AsyncSelect)
3)事件選擇模型(EventSeselect)
其思想是根據io類型,預先查看1個或n個socket是否能讀、寫等。
其select本身來說,select是阻塞的,可以同時監視多個socket,只要所監視的其中一個socket可以讀、寫,secect調用才返回
非同步選擇模型其select是非同步的(非同步是不會阻塞的),是將監視任務委託給系統,系統在socket可讀、寫時通過消息通知應用程序。有一點需要說明,假如應用程序已經有很多數據需要發送,當收到可寫通知時,一定要盡量多地發送數據,直到發送失敗,lasterror提示「將要阻塞」,將來才可能有新的可寫通知到來,否則永遠也不會有。
事件選擇模型也是將監視socket狀態的工作委託給系統,系統在適當的時候通過事件通知應用程序socket可以的操作。
除了同步工作方式外,還有一種叫非同步工作方式
非同步工作方式是不會阻塞的,因為是將io操作本身委託給系統,系統在io操作完成後通過回調常式或事件或完成包通知應用程序
非同步工作方式有兩種io模型和其對應,其實這兩種模型是window是非同步io的實現:
1)重疊模型
2)完成埠
重疊模型通過事件或回調常式通知應用程序io已經完成
完成埠模型比較復雜,完成埠本身其實是一個io完成包隊列。
應用程序一般創建若干個線程用來監視完成埠,這些線程試圖從完成埠移除一個完成包,如果有,移除成功,應用程序處理該完成包,否則應用程序監視完成埠的線程被阻塞。
select模型是從UNIX上的Berkeley Software Distribution(BSD)版本的套接字就實現了的,其它四種io模型windows發明的,在windows中完成埠和非同步選擇模型是使用比較廣泛的,一般分別用於服務端和客戶端開發。
這五種io模型設計還是比較巧妙的:三種選擇模型很好解決了「同步非阻塞」模式編程的不足;重疊模型和完成埠是windows非同步io的經典實現,不局限於網路io,對文件io同樣適用。
說點題外話,socket的send完成僅僅是將數據(可能是部分)提交給系統,而不是已經發送到了網卡上,更不是已經發送到了接收端。所以要知道你的數據已經發送到了對方的應用層的唯一方法是,讓對方給你發送一個應對包。
發送數據要注意,對應tcp,要防止發送和接收的亂序,對於發送,一般應該為每一個鏈接建立一個發送隊列,採用類似nagle的演算法啟動數據發送。
一次發送可能是你提交數據的一部分,一定要當心,否則出問題沒處找去。
⑻ 快速排序演算法的排序演示
假設用戶輸入了如下數組: 下標 0 1 2 3 4 5 數據 6 2 7 3 8 9 創建變數i=0(指向第一個數據), j=5(指向最後一個數據), k=6(賦值為第一個數據的值)。
我們要把所有比k小的數移動到k的左面,所以我們可以開始尋找比6小的數,從j開始,從右往左找,不斷遞減變數j的值,我們找到第一個下標3的數據比6小,於是把數據3移到下標0的位置,把下標0的數據6移到下標3,完成第一次比較: 下標 0 1 2 34 5 數據 3 2 7 6 8 9 i=0 j=3 k=6
接著,開始第二次比較,這次要變成找比k大的了,而且要從前往後找了。遞加變數i,發現下標2的數據是第一個比k大的,於是用下標2的數據7和j指向的下標3的數據的6做交換,數據狀態變成下表: 下標 0 1 2 3 4 5 數據 3 2 6 7 8 9 i=2 j=3 k=6
稱上面兩次比較為一個循環。
接著,再遞減變數j,不斷重復進行上面的循環比較。
在本例中,我們進行一次循環,就發現i和j「碰頭」了:他們都指向了下標2。於是,第一遍比較結束。得到結果如下,凡是k(=6)左邊的數都比它小,凡是k右邊的數都比它大: 下標 0 1 2 3 4 5 數據 3 2 6 7 8 9 如果i和j沒有碰頭的話,就遞加i找大的,還沒有,就再遞減j找小的,如此反復,不斷循環。注意判斷和尋找是同時進行的。
然後,對k兩邊的數據,再分組分別進行上述的過程,直到不能再分組為止。
注意:第一遍快速排序不會直接得到最終結果,只會把比k大和比k小的數分到k的兩邊。為了得到最後結果,需要再次對下標2兩邊的數組分別執行此步驟,然後再分解數組,直到數組不能再分解為止(只有一個數據),才能得到正確結果。 在c++中可以用函數qsort()可以直接為數組進行排序。
用 法:
void qsort(void *base, int nelem, int width, int (*fcmp)(const void *,const void *));
參數:1 待排序數組首地址2 數組中待排序元素數量3 各元素的佔用空間大小4 指向函數的指針,用於確定排序的順序
⑼ C語言中快速排序法的原理及應用
「快速排序法」使用的是遞歸原理,下面我結合一個例子來說明「快速排序法」的原理。首先給出一個數組{53,12,98,63,18,72,80,46, 32,21},先找到第一個數--53,把它作為中間值,也就是說,要把53放在一個位置,使得它左邊的值比它小,右邊的值比它大。{21,12,32, 46,18,53,80,72,63,98},這樣一個數組的排序就變成了兩個小數組的排序--53左邊的數組和53右邊的數組,而這兩個數組繼續用同樣的方式繼續下去,一直到順序完全正確。
一般來說,冒泡法是程序員最先接觸的排序方法,它的優點是原理簡單,編程實現容易,但它的缺點就是--程序的大忌--速度太慢。
附上快速排序代碼:
#include<stdio.h>
voidquicksort(inta[],intleft,intright)
{
inti,j,temp;
i=left;
j=right;
temp=a[left];
if(left>right)
return;
while(i!=j)
{
while(a[j]>=temp&&j>i)
j--;
if(j>i)
a[i++]=a[j];
while(a[i]<=temp&&j>i)
i++;
if(j>i)
a[j--]=a[i];
}
a[i]=temp;
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
voidmain()
{
inta[]={53,12,98,63,18,72,80,46,32,21};
inti;
quicksort(a,0,9);
/*排好序的結果*/
for(i=0;i<10;i++)
printf("%4d ",a[i]);
}
⑽ 冒泡排序法和快速排序比較的演算法
打你屁股,這么簡單的問題都不認真研究一下。
冒泡排序是最慢的排序,時間復雜度是 O(n^2)。
快速排序是最快的排序。關於快速排序,我推薦你看看《代碼之美》第二章:我編寫過的最漂亮的代碼。作者所說的最漂亮,就是指效率最高的。
--------------------------------摘自《代碼之美》---------------
當我撰寫關於分治(divide-and-conquer)演算法的論文時,我發現C.A.R. Hoare的Quicksort演算法(「Quicksort」,Computer Journal 5)無疑是各種Quicksort演算法的鼻祖。這是一種解決基本問題的漂亮演算法,可以用優雅的代碼實現。我很喜歡這個演算法,但我總是無法弄明白演算法中最內層的循環。我曾經花兩天的時間來調試一個使用了這個循環的復雜程序,並且幾年以來,當我需要完成類似的任務時,我會很小心地復制這段代碼。雖然這段代碼能夠解決我所遇到的問題,但我卻並沒有真正地理解它。
我後來從Nico Lomuto那裡學到了一種優雅的劃分(partitioning)模式,並且最終編寫出了我能夠理解,甚至能夠證明的Quicksort演算法。William Strunk Jr.針對英語所提出的「良好的寫作風格即為簡練」這條經驗同樣適用於代碼的編寫,因此我遵循了他的建議,「省略不必要的字詞」(來自《The Elements of Style》一書)。我最終將大約40行左右的代碼縮減為十幾行的代碼。因此,如果要回答「你曾編寫過的最漂亮代碼是什麼?」這個問題,那麼我的答案就是:在我編寫的《Programming Pearls, Second Edition》(Addison-Wesley)一書中給出的Quichsort演算法。在示例2-1中給出了用C語言編寫的Quicksort函數。我們在接下來的章節中將進一步地研究和改善這個函數。
【示例】 2-1 Quicksort函數
void quicksort(int l, int u)
{ int i, m;
if (l >= u) return; 10
swap(l, randint(l, u));
m = l;
for (i = l+1; i <= u; i++)
if (x[i] < x[l])
swap(++m, i);
swap(l, m);
quicksort(l, m-1);
quicksort(m+1, u);
}
如果函數的調用形式是quicksort(0, n-1),那麼這段代碼將對一個全局數組x[n]進行排序。函數的兩個參數分別是將要進行排序的子數組的下標:l是較低的下標,而u是較高的下標。函數調用swap(i,j)將會交換x[i]與x[j]這兩個元素。第一次交換操作將會按照均勻分布的方式在l和u之間隨機地選擇一個劃分元素。
在《Programming Pearls》一書中包含了對Quicksort演算法的詳細推導以及正確性證明。在本章的剩餘內容中,我將假設讀者熟悉在《Programming Pearls》中所給出的Quicksort演算法以及在大多數初級演算法教科書中所給出的Quicksort演算法。
如果你把問題改為「在你編寫那些廣為應用的代碼中,哪一段代碼是最漂亮的?」我的答案還是Quicksort演算法。在我和M. D. McIlroy一起編寫的一篇文章("Engineering a sort function," Software-Practice and Experience, Vol. 23, No. 11)中指出了在原來Unix qsort函數中的一個嚴重的性能問題。隨後,我們開始用C語言編寫一個新排序函數庫,並且考慮了許多不同的演算法,包括合並排序(Merge Sort)和堆排序(Heap Sort)等演算法。在比較了Quicksort的幾種實現方案後,我們著手創建自己的Quicksort演算法。在這篇文章中描述了我們如何設計出一個比這個演算法的其他實現要更為清晰,速度更快以及更為健壯的新函數——部分原因是由於這個函數的代碼更為短小。Gordon Bell的名言被證明是正確的:「在計算機系統中,那些最廉價,速度最快以及最為可靠的組件是不存在的。」現在,這個函數已經被使用了10多年的時間,並且沒有出現任何故障。
考慮到通過縮減代碼量所得到的好處,我最後以第三種方式來問自己在本章之初提出的問題。「你沒有編寫過的最漂亮代碼是什麼?」。我如何使用非常少的代碼來實現大量的功能?答案還是和Quicksort有關,特別是對這個演算法的性能分析。我將在下一節給出詳細介紹。
2.2 事倍功半
Quicksort是一種優雅的演算法,這一點有助於對這個演算法進行細致的分析。大約在1980年左右,我與Tony Hoare曾經討論過Quicksort演算法的歷史。他告訴我,當他最初開發出Quicksort時,他認為這種演算法太簡單了,不值得發表,而且直到能夠分析出這種演算法的預期運行時間之後,他才寫出了經典的「Quicksoft」論文。
我們很容易看出,在最壞的情況下,Quicksort可能需要n2的時間來對數組元素進行排序。而在最優的情況下,它將選擇中值作為劃分元素,因此只需nlgn次的比較就可以完成對數組的排序。那麼,對於n個不同值的隨機數組來說,這個演算法平均將進行多少次比較?
Hoare對於這個問題的分析非常漂亮,但不幸的是,其中所使用的數學知識超出了大多數程序員的理解范圍。當我為本科生講授Quicksort演算法時,許多學生即使在費了很大的努力之後,還是無法理解其中的證明過程,這令我非常沮喪。下面,我們將從Hoare的程序開
11
始討論,並且最後將給出一個與他的證明很接近的分析。
我們的任務是對示例2-1中的Quicksort代碼進行修改,以分析在對元素值均不相同的數組進行排序時平均需要進行多少次比較。我們還將努力通過最短的代碼、最短運行時間以及最小存儲空間來得到最深的理解。
為了確定平均比較的次數,我們首先對程序進行修改以統計次數。因此,在內部循環進行比較之前,我們將增加變數comps的值(參見示例2-2)。
【示例2-2】 修改Quicksort的內部循環以統計比較次數。
for (i = l+1; i <= u; i++) {
comps++;
if (x[i] < x[l])
swap(++m, i);
}
如果用一個值n來運行程序,我們將會看到在程序的運行過程中總共進行了多少次比較。如果重復用n來運行程序,並且用統計的方法來分析結果,我們將得到Quicksort在對n個元素進行排序時平均使用了1.4 nlgn次的比較。
在理解程序的行為上,這是一種不錯的方法。通過十三行的代碼和一些實驗可以反應出許多問題。這里,我們引用作家Blaise Pascal和T. S. Eliot的話,「如果我有更多的時間,那麼我給你寫的信就會更短。」現在,我們有充足的時間,因此就讓我們來對代碼進行修改,並且努力編寫出更短(同時更好)的程序。
我們要做的事情就是提高這個演算法的速度,並且盡量增加統計的精確度以及對程序的理解。由於內部循環總是會執行u-l次比較,因此我們可以通過在循環外部增加一個簡單的操作來統計比較次數,這就可以使程序運行得更快一些。在示例2-3的Quicksort演算法中給出了這個修改。
【示例2-3】 Quicksort的內部循環,將遞增操作移到循環的外部
comps += u-l;
for (i = l+1; i <= u; i++)
if (x[i] < x[l])
swap(++m, i);
這個程序會對一個數組進行排序,同時統計比較的次數。不過,如果我們的目標只是統計比較的次數,那麼就不需要對數組進行實際地排序。在示例2-4中去掉了對元素進行排序的「實際操作」,而只是保留了程序中各種函數調用的「框架」。
【示例2-4】將Quicksort演算法的框架縮減為只進行統計
void quickcount(int l, int u)
{ int m;
if (l >= u) return;
m = randint(l, u);
comps += u-l;
quickcount(l, m-1);
quickcount(m+1, u);
}
12
這個程序能夠實現我們的需求,因為Quichsort在選擇劃分元素時採用的是「隨機」方式,並且我們假設所有的元素都是不相等的。現在,這個新程序的運行時間與n成正比,並且相對於示例2-3需要的存儲空間與n成正比來說,現在所需的存儲空間縮減為遞歸堆棧的大小,即存儲空間的平均大小與lgn成正比。
雖然在實際的程序中,數組的下標(l和u)是非常重要的,但在這個框架版本中並不重要。因此,我們可以用一個表示子數組大小的整數(n)來替代這兩個下標(參見示例2-5)
【示例2-5】 在Quicksort代碼框架中使用一個表示子數組大小的參數
void qc(int n)
{ int m;
if (n <= 1) return;
m = randint(1, n);
comps += n-1;
qc(m-1);
qc(n-m);
}
現在,我們可以很自然地把這個過程整理為一個統計比較次數的函數,這個函數將返回在隨機Quicksort演算法中的比較次數。在示例2-6中給出了這個函數。
【示例2-6】 將Quicksort框架實現為一個函數
int cc(int n)
{ int m;
if (n <= 1) return 0;
m = randint(1, n);
return n-1 + cc(m-1) + cc(n-m);
}
在示例2-4、示例2-5和示例2-6中解決的都是相同的基本問題,並且所需的都是相同的運行時間和存儲空間。在後面的每個示例都對這些函數的形式進行了改進,從而比這些函數更為清晰和簡潔。
在定義發明家的矛盾(inventor's paradox)(How To Solve It, Princeton University Press)時,George Póllya指出「計劃越宏大,成功的可能性就越大。」現在,我們就來研究在分析Quicksort時的矛盾。到目前為止,我們遇到的問題是,「當Quicksort對大小為n的數組進行一次排序時,需要進行多少次比較?」我們現在將對這個問題進行擴展,「對於大小為n的隨機數組來說,Quichsort演算法平均需要進行多少次的比較?」我們通過對示例2-6進行擴展以引出示例2-7。
【示例2-7】 偽碼:Quicksort的平均比較次數
float c(int n)
if (n <= 1) return 0
sum = 0
for (m = 1; m <= n; m++)
sum += n-1 + c(m-1) + c(n-m)
return sum/n
如果在輸入的數組中最多隻有一個元素,那麼Quichsort將不會進行比較,如示例2-6
13
中所示。對於更大的n,這段代碼將考慮每個劃分值m(從第一個元素到最後一個,每個都是等可能的)並且確定在這個元素的位置上進行劃分的運行開銷。然後,這段代碼將統計這些開銷的總和(這樣就遞歸地解決了一個大小為m-1的問題和一個大小為n-m的問題),然後將總和除以n得到平均值並返回這個結果。
如果我們能夠計算這個數值,那麼將使我們實驗的功能更加強大。我們現在無需對一個n值運行多次來估計平均值,而只需一個簡單的實驗便可以得到真實的平均值。不幸的是,實現這個功能是要付出代價的:這個程序的運行時間正比於3n(如果是自行參考(self-referential)的,那麼用本章中給出的技術來分析運行時間將是一個很有趣的練習)。
示例2-7中的代碼需要一定的時間開銷,因為它重復計算了中間結果。當在程序中出現這種情況時,我們通常會使用動態編程來存儲中間結果,從而避免重復計算。因此,我們將定義一個表t[N+1],其中在t[n]中存儲c[n],並且按照升序來計算它的值。我們將用N來表示n的最大值,也就是進行排序的數組的大小。在示例2-8中給出了修改後的代碼。
【示例2-8】 在Quicksort中使用動態編程來計算
t[0] = 0
for (n = 1; n <= N; n++)
sum = 0
for (i = 1; i <= n; i++)
sum += n-1 + t[i-1] + t[n-i]
t[n] = sum/n
這個程序只對示例2-7進行了細微的修改,即用t[n]來替換c(n)。它的運行時間將正比於N2,並且所需的存儲空間正比於N。這個程序的優點之一就是:在程序執行結束時,數組t中將包含數組中從元素0到元素N的真實平均值(而不是樣本均值的估計)。我們可以對這些值進行分析,從而生成在Quichsort演算法中統計比較次數的計算公式。
我們現在來對程序做進一步的簡化。第一步就是把n-1移到循環的外面,如示例2-9所示。
【示例2-9】 在Quicksort中把代碼移到循環外面來計算
t[0] = 0
for (n = 1; n <= N; n++)
sum = 0
for (i = 1; i <= n; i++)
sum += t[i-1] + t[n-i]
t[n] = n-1 + sum/n
現在將利用對稱性來對循環做進一步的調整。例如,當n為4時,內部循環計算總和為:
t[0]+t[3] + t[1]+t[2] + t[2]+t[1] + t[3]+t[0]
在上面這些組對中,第一個元素增加而第二個元素減少。因此,我們可以把總和改寫為:
2 * (t[0] + t[1] + t[2] + t[3])
我們可以利用這種對稱性來得到示例2-10中的Quicksort。
【示例2-10】 在Quichsort中利用了對稱性來計算
t[0] = 0
14
for (n = 1; n <= N; n++)
sum = 0
for (i = 0; i < n; i++)
sum += 2 * t[i]
t[n] = n-1 + sum/n
然而,在這段代碼的運行時間中同樣存在著浪費,因為它重復地計算了相同的總和。此時,我們不是把前面所有的元素加在一起,而是在循環外部初始化總和並且加上下一個元素,如示例2-11所示。
【示例2-11】 在Quicksort中刪除了內部循環來計算
sum = 0; t[0] = 0
for (n = 1; n <= N; n++)
sum += 2*t[n-1]
t[n] = n-1 + sum/n
這個小程序確實很有用。程序的運行時間與N成正比,對於每個從1到N的整數,程序將生成一張Quicksort的估計運行時間表。
我們可以很容易地把示例2-11用表格來實現,其中的值可以立即用於進一步的分析。在2-1給出了最初的結果行。
表2-1 示例2-11中實現的表格輸出
N Sum t[n]
0 0 0
1 0 0
2 0 1
3 2 2.667
4 7.333 4.833
5 17 7.4
6 31.8 10.3
7 52.4 13.486
8 79.371 16.921
這張表中的第一行數字是用代碼中的三個常量來進行初始化的。下一行(輸出的第三行)的數值是通過以下公式來計算的:
A3 = A2+1 B3 = B2 + 2*C2 C3 = A2-1 + B3/A3
把這些(相應的)公式記錄下來就使得這張表格變得完整了。這張表格是「我曾經編寫的最漂亮代碼」的很好的證據,即使用少量的代碼完成大量的工作。
但是,如果我們不需要所有的值,那麼情況將會是什麼樣?如果我們更希望通過這種來方式分析一部分數值(例如,在20到232之間所有2的指數值)呢?雖然在示例2-11中構建了完整的表格t,但它只需要使用表格中的最新值。因此,我們可以用變數t的定長空間來替代table t[]的線性空間,如示例2-12所示。
【示例2-12】 Quicksoft 計算——最終版本
sum = 0; t = 0
15
for (n = 1; n <= N; n++)
sum += 2*t
t = n-1 + sum/n
然後,我們可以插入一行代碼來測試n的適應性,並且在必要時輸出這些結果。
這個程序是我們漫長學習旅途的終點。通過本章所採用的方式,我們可以證明Alan Perlis的經驗是正確的:「簡單性並不是在復雜性之前,而是在復雜性之後」 ("Epigrams on Programming," Sigplan Notices, Vol. 17, Issue 9)。