除了剛邁出校門找工做那會兒對基本排序算法還算「瞭然於心」,隨着工做和時間的推移,當回頭再來看這些基礎的不能再基礎的東西時,絕大多數人沒法寫出經典排序算法的核心代碼,甚至連算法原理都忘了。我認可,本身就是這樣的人,因此今天有空將常見的幾種排序算法複習一下,寫個筆記。一方面給本身一個「從新作人」的機會,另外一方面也歡迎酷愛算法的朋友一塊兒交流分享。
常見的排序算法有選擇排序、
冒泡排序、插入排序、希爾排序、歸併排序、堆排序、快速排序這些都是之前教科書上教給咱們的。科技在發展,人類在進步,在前人們不懈努力下新的排序算法老是層出不窮,特別是大數據時代關於海量數據的處理方面顯得尤其重要,因此出現了諸如計數排序(couting sort)、桶排序(bucket sort)、基數排序(radix sort)。這些暫不屬於咱們的討論範圍。
選擇排序
所謂的「選擇」就是在待排序列裏,找出一個最大(小)的元素,而後將它放在序列某個位置,這就完成了一次選擇過程。若是將這樣的選擇循環繼續下去,就是咱們所說的選擇排序。這也是選擇排序的精髓。
注:本文所討論的都是升序排序,降序也同樣,沒啥本質區別。
假如,有一個無須序列A={6,3,1,9,2,5,8,7,4},選擇排序的過程應該以下:
第一趟:選擇最小的元素,而後將其放置在數組的第一個位置A[0],將A[0]=6和A[2]=1進行交換,此時A={1,3,6,9,2,5,8,7,4};
第二趟:因爲A[0]位置上已是最小的元素了,因此此次從A[1]開始,在剩下的序列裏再選擇一個最小的元素將其與A[1]進行交換。即這趟選擇過程找到了最小元素A[4]=2,而後與A[1]=3進行交換,此時A={1,2,
6
,9,3,5,8,7,4};
第三趟:因爲A[0]、A[1]已經有序,因此在A[2]~A[8]裏再選擇一個最小元素與A[2]進行交換,而後將這個過程一直循環下去直到A裏全部的元素都排好序爲止。這就是選擇排序的精髓。所以,咱們很容易寫出選擇排序的核心代碼部分,即選擇的過程,就是不斷的比較、交換的過程。
整個選擇的過程以下圖所示:
其中,黃色表示即將要選擇的位置,即該位置上要安放從該位置日後開始最小的那個元素;桔色表示在無須序列裏最小元素所在的位置,通過交換後的結果如右邊箭頭所示。而全部的灰色塊,表示那些位置上的元素已經排好序了。因此,整個選擇排序算法的核心代碼以下:
- int min,tmp,i,j;
- for(i=0;i<len-1;i++){
- min = i; //在本趟選擇過程當中,咱們要將最小的元素放在a[i]的位置上
-
- for(j=i+1;j<len;j++) /××××××××××××××××××××××××××××××××××××××××××××××××××××××××
- if(a[min]>a[j]) ×在剩下的len-i個元素裏選擇一個最小,而後用min記住其下標
- min = j; ××××××××××××××××××××××××××××××××××××××××××××××××××××××××/
-
- /× 若是a[i]自己就已是最小的元素,則不要交換,這樣能夠提升一點算法的效率×/
- if(min != i){
- swap(a[min],a[i]) //交換兩個數
- }
- }
咱們能夠看到,不論待排序列是否有序,選擇排序算法都要進行大量的比較、交換。第一趟選擇過程當中,須要比較n-1次,第二趟須要比較n-2次,第i次就須要n-i次比較,因此最後第n-1個元素只須要和第n個元素比較一次就能夠了。若是咱們將交換兩個元素所耗費的時間認爲是個常數忽略不計,每次比較所耗費的時間又是固定單位時間,那麼整個選擇排序所耗費的時間就是:
1+2+3+4+5+... ...+(n-2)+(n-1)=n*(n-1)/2,其時間複雜度是O(n^2)。任何狀況下,不管待排序列是否有序,選擇排序所耗費的比較時間都是O(n^2)。惟一的區別在於,若是待排序列已是升序的,那麼最好的狀況就是不須要任何交換;若是待排序列是逆序的,則最壞的狀況就是總共須要交換n/2次。
因此,選擇排序算法,最好、最壞和平均時間複雜度都是O(n^2)。
冒泡排序
冒泡排序算法的核心是每次冒泡過程當中,比較相鄰的兩個元素,若是A[i]大於A[i+1],則將其交換,而後A[i+1]和A[i+2]再進行比較,將大的元素日後放。這樣一趟下來,最大元素就被逐次「冒」到序列的末尾了。繼續之前面的序列A爲例:咱們將序列「豎起來」,這樣看冒泡的效果會更好。
第五趟沒有畫出來,實際上第五趟冒泡完了以後,整個序列就已經升序排列好了。上圖中,在每一趟冒泡過程當中,綠色都表示本趟、本次相鄰兩個元素比較後值較大者,而紫紅色則是較小者。一樣滴,咱們也很容易就寫出冒泡排序的核心算法:
- int i;
- while(len--){
- for(i=0;i<len;i++){
- if(a[i]>a[i+1]){
- swap(a[i],a[i+1]); //交換兩個數
- }
- }
- }
由於a[i]要和a[i+1]進行比較,因此得確保i+1的值不會形成數組訪問越界的狀況。第一趟比較時序號i+1最大能夠指向序列最後一個元素,即i+1=len-1,i此時的值即爲i=len-2。注意理解上述代碼第2行的while(len--)在冒泡排序算法上的用意。
關於冒泡排序的時間複雜度,若是序列的長度爲n,第一趟的冒泡須要的比較次數是n-1,第二趟冒泡比較時最後一個元素已經脫穎而出,因此第二趟冒泡的總比較次數是n-2,以此類推,第i次冒泡的比較次數是n-i。一樣地,和選擇排序同樣,冒泡排序在最壞和最好的狀況下,其時間複雜度都是O(n^2)。
讓咱們把冒泡排序第五趟及剩下的過程再分析一下:
雖然咱們看到第五趟冒泡完成後,整個序列都已經有序了,但接下來的第6、第7、第八和第九趟冒泡過程依然要作相鄰元素比較的無用功。基於此,有人提出了改進的冒泡排序算法。算法主體和傳統的冒泡排序一致,改進之處在於將上述這種無用的比較操做給濾掉了。其核心思想是,在冒泡排序過程當中,若是有一趟冒泡過程當中沒有發生交換操做,則待排序的序列必定已經有序了。這個結論能夠用數學概括法來證實,有興趣的朋友能夠去研究一下。反應到程序層面就是咱們須要用一個標記變量來記錄某趟排序是否有交換操做發生,若是有則繼續冒泡的過程;若是沒有則當即中止冒泡,此時待排序的序列必定已經有序了。代碼的改動也很簡單:
- int i,goon=1;
- while(goon && len--){
- goon=0;
- for(i=0;i<len;i++){
- if(a[i]>a[i+1]){
- swap(a[i],a[i+1]); //交換兩個數
- goon = 1;
- }
- }
- }
改進以後的冒泡排序,時間複雜度最好的狀況能夠達到O(n),最壞的狀況依然是O(n^2)。 未完,待續...