轉載自:
各種排序算法的實現及其比較
排序算法是筆試和麪試中最喜歡考到的內容,今晚花了好幾個小時的時間把之前接觸過的排序算法都重新實現了一遍。
主要是作爲複習用。當然也希望能夠給大家幫上點忙。
對各種排序算法比較熟悉的朋友可以直接跳過。
常用的內部排序算法主要分爲五類:插入、交換、選擇、歸併、基數排序。
文章的最後可能還會稍微分析一下外部排序。。。內/外部排序的區別就是 外部排序指的是大文件的排序,即待排序的記錄存儲在外存儲器上,在排序過程中需要多次的內/外存之間的交換。
下面一個一個分析。
(注意,本篇中講的 lg(n) 都是以2爲底的)
一、插入排序
下面講到的這些插入排序的時間複雜度,除希爾排序是O(n的3/2次方)外,其它的都是O(n平方)。另一方面,除希爾排序外,其它的排序都是穩定的。(穩定是指相同的兩個數在排序之後它們的相對位置不變。)
1、直接插入排序。
這是最簡單的排序方法,它的基本基本操作是將一個記錄插入到已排好序的有序表中,從而得到一個新的、記錄數增加1的有序表。
- #include<iostream>
- using namespace std;
- int main()
- {
- int src[] = {49,38,65,97,76,13,27,49};
- int i,j,cnt = sizeof(src)/4;
- int guard = src[0]; //設置一個哨兵,用來記錄當前值
- for(i = 1; i < cnt; i ++)
- {
- if(src[i] < src[i-1])
- {
- guard = src[i];
- src[i] = src[i-1];
- for(j = i - 2; src[j]>guard; j --) src[j+1] = src[j];
- src[j+1] = guard;
- }
- }
- for(i = 0; i < cnt; i ++) cout<<src[i]<<endl;
- //getchar();
- return 0;
- }
2、折半插入排序
這種插入排序是上一種排序的改進,就是在查找的時候不用一個一個對比,因爲前面已經有序,所以可以用折半法。
- #include<iostream>
- using namespace std;
- int main()
- {
- int src[] = {49,38,65,97,76,13,27,49};
- int i,j,low,high,mid,cnt = sizeof(src)/4;
- int guard = src[0];
- for(i = 1; i < cnt; i ++)
- {
- if(src[i] < src[i-1])
- {
- guard = src[i];
- low = 0, high = i - 1;
- while(low <= high)
- {
- mid = (low+high)/2;
- if(guard < src[mid]) high = mid - 1;
- else low = mid+1;
- }
- for(j = i - 1; j >= high +1; j --) src[j+1] = src[j]; //後移
- src[j+1] = guard;
- }
- }
- for(i = 0; i < cnt; i ++) cout<<src[i]<<endl;
- //getchar();
- return 0;
- }
3、2-路插入排序
先讀入前面兩個數,大的放隊首,小的放隊尾,並分別用final和first作爲指針指向它們,兩個都向中間延伸,first向右增長,final向左減小。
如對 int src[] = {49,38,65,97,76,13,27,49_} 進行排序(這裏爲了區分兩個49,
我給第二個49加了一個下劃線。。。)
第一步得:
final | first | ||||||
49 | x | x | x | x | x | x | 38 |
放入第三個數時,和隊首first和隊尾final分別進行比較,如果比first大則放它右邊,並用first指向它。如果比final小,則放final的左邊,並用final指向它。如果大小final,小於first,則插入到當前的first或final的位置。
第二步得:
final | first | ||||||
49 | 65 | x | x | x | x | x | 38 |
第三步:
final | first | ||||||
49 | 65 | 97 | x | x | x | x | 38 |
第四步:
final | first | ||||||
49 | 65 | 76 | 97 | x | x | x | 38 |
第五步:
final | first | ||||||
49 | 65 | 76 | 97 | x | x | 13 | 38 |
第六步:
final | first | ||||||
49 | 65 | 76 | 97 | x | 13 | 27 | 38 |
第七步:
final | first | ||||||
49 | 49_ | 65 | 76 | 97 | 13 | 27 | 38 |
這樣做的好處是可以減少移動的次數。。。具體的程序實現留給讀者去實現。。。
4、希爾排序
又稱爲 縮小增量排序,它的基本思想是,先將整個待排記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄“基本”有序時,再對全體記錄進行一次直接插入排序。
以 int src[] = {49,38,65,97,76,13,27,49_,55,4} 爲例,共有10個數,
我們先以10/2=5爲跨度,進行第一趟排序。
第一趟:
49和13比較,38和27比較,65和49_比較,97和55比較,76和4比較,得:
13,27,49_,55,4,49,38,65,97,76 (從這裏可以看出,希爾排序是不穩定的,當然下結論前還要看最後的結果。)
第二趟,我們以5-2=3爲跨度,進行排序,可得:
13,4,49_,38,27,49,55,65,97,76 (到這時一看,有序多了!)
第三趟,心3-2=1爲跨度,進行排序,得:
4,13,27,38,49_,49,55,65,76,97
當跨度減小到1時,就算是排序結束了。由結果可以斷定,希爾排序是不穩定的排序。
- #include<iostream>
- using namespace std;
- int main()
- {
- int src[] = {49,38,65,97,76,13,27,49,55,4}; //大家可試一試奇數個的情況
- int i,j,tmp,cnt = sizeof(src)/4,dk = (cnt+1)/2,c=1;
- while(dk > 0)
- {
- cout<<c++<<endl;
- for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
- cout<<endl;
- for(i = dk; i < cnt; i ++)
- {
- if(src[i] < src[i-dk])
- {
- tmp = src[i];
- src[i] = src[i-dk];
- src[i-dk] = tmp;
- }
- }
- dk = dk - 2 == 0 ? 1:dk-2;
- }
- cout<<c<<endl;
- for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
- cout<<endl;
- getchar();
- return 0;
- }
二、交換排序
1、冒泡排序
很簡單,就是相鄰的兩個數,兩兩相互比較,其特點是:每比較一次就可以得出最小/大的一個數 放到隊首或隊尾。它的時間複雜度也比較大:O(n^2),冒泡排序算法是穩定的排序方式。
- #include<iostream>
- using namespace std;
- int main()
- {
- int src[] = {49,38,65,97,76,13,27,49};
- int i,j,cnt = sizeof(src)/4;
- for(i = 0; i < cnt; i ++)
- for(j = cnt-1; j > i; j --)
- if(src[j] < src[j-1]) swap(src[j],src[j-1]);
- //輸出
- for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
- cout<<endl;
- getchar();
- return 0;
- }
2、快速排序
快速排序的時間複雜度達到O(nlgn),被公認爲最快的排序方法之一。在所有同數量級(O(nlgn))的排序當中,其平均性能最好。它其實是冒泡排序的改進,當一列數據基本有序的時候,快速排序將爲蛻化爲冒泡排序,時間複雜度爲O(n平方)。基本思想是 取一個數作爲中間數,比它小的都排左邊,比它大的都排右邊(如果是從大到小排序的話,就反過來),再對每一邊用同樣的思路進行遞歸求解。快速排序是不穩定的排序方式。
- #include<iostream>
- using namespace std;
- void quickSort(int *src, int low,int high)
- {
- if(src == NULL) return ;
- int i,j,pivot;
- if(low < high)
- {
- pivot = src[low];
- i = low,j = high;
- while(i < j)
- {
- while(i<j && src[j] >= pivot) j--;
- src[i] = src[j];
- while(i<j && src[i] <= pivot) i++;
- src[j] = src[i];
- }
- src[i] = pivot;
- quickSort(src,low,i-1);
- quickSort(src,i+1,high);
- }
- }
- int main()
- {
- int src[] = {49,38,65,97,76,13,27,49};
- int i,low,high,cnt = sizeof(src)/4;
- quickSort(src,0,cnt-1);
- //輸出
- for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
- cout<<endl;
- getchar();
- return 0;
- }
三、選擇排序
1、簡單選擇排序
思路很簡單,就是每次選出最小/大的數,和前面的第i個數交換。時間複雜度也是 O(n平方),是不穩定的排序方式,因爲在交換的過程中,相同的兩個數,前者有可能被交換到後面去,舉個例子,序列5 8 5 2 9, 我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對前後順序就被破壞了,所以選擇排序不是一個穩定的排序算法。
- #include<iostream>
- using namespace std;
- int main()
- {
- int src[] = {49,38,65,97,76,13,27,49};
- int i,j,cnt = sizeof(src)/4,min ,index;
- for(i = 0; i < cnt; i ++)
- {
- min = INT_MAX,index = -1;
- for(j = i; j < cnt; j ++)
- {
- if(min > src[j])
- {
- min = src[j];
- index = j;
- }
- }
- if(index != i)
- {
- src[index] = src[i];
- src[i] = min;
- }
- }
- //輸出
- for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
- cout<<endl;
- getchar();
- return 0;
- }
2、堆排序
先用給定的數構造一棵完全二叉樹,然後從下標爲cnt/2(cnt指給定的數的個數)開始一個一個和它的孩子結點比較,小的就往上挪,最後得到一個小頂堆,取出堆頂,並把最後一個數放入堆頂,進行同樣的操作,直到所有的數都已取完爲止。我們可以用一個數組來順序地表示這棵樹,左孩子可以通過2*n來找到,右孩子可以通過2*n+1來找到。 下面給一個例子:
int src[] = {49,38,65,97,76,13,27,49};
堆排序的時間複雜度是O(nlgn),也是最快的排序方法之一,在最壞的情況下,其時間複雜度還是O(nlgn),相對於快速排序來說,這是堆排序的最大優點。此外,堆排序僅需要一個記錄大小供交換用的輔助存儲空間。堆排序也是不穩定的排序。
- #include<iostream>
- using namespace std;
- void heapAdjust(int *src, int s,int m)
- { //從s開始進行一次調整,其中m指向需要進行排序的數據中最大的下標
- if(src == NULL) return;
- int i,rc = src[s];
- for(i = 2*s+1; i <= m; i = 2*i+1) //一層一層往下走
- {
- if(i<m && src[i] < src[i+1]) i++; //讓i指向最大的那個孩子
- if(rc >= src[i]) break;
- src[s] = src[i];
- s = i;
- }
- src[s] = rc;
- }
- int main()
- {
- int src[] = {49,38,65,97,76,13,27,49,1};
- int i,j,cnt = sizeof(src)/4;
- //先構建一個小頂堆
- for(i = cnt/2-1; i >-1; i --)
- heapAdjust(src,i,cnt-1);
- //每次取出頂部最大的那個數放後面,再進行一次頂點調整
- for(i = cnt-1; i > 0; i --)
- {
- swap(src[0],src[i]);
- heapAdjust(src,0,i-1);
- }
- //輸出
- for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
- cout<<endl;
- getchar();
- return 0;
- }
四、歸併排序
歸併的意思就是兩個或兩個以上的有序表組合成一個新的有序表。整個歸併排序需要進行【lgn取上限】次,總的時間複雜度爲O(nlgn)。與快速排序相比,歸併排序的最大特點是:它是一種穩定的排序方法。
如上圖,這是一種2-路歸併排序,通常用遞歸方法來實現,遞歸的形式的算法在形式上較簡潔,但實用性很差。
- #include<iostream>
- using namespace std;
- void merge(int* src,int i,int m,int n)
- {
- //開闢一個臨時的空間來存放歸併後的結果
- int *des = (int*)malloc(sizeof(int)*n);
- int k = i,mm = m++,low = i;
- //把數組的兩部分,一個一個地放入臨時數組中,小的先放
- while(i <= mm && m <= n)
- if(src[i] <= src[m]) des[k++] = src[i++];
- else des[k++] = src[m++];
- //把剩餘的放入數組
- while(i <= mm) des[k++] = src[i++];
- while(m <= n) des[k++] = src[m++];
- //把結果拷貝回原數組
- for(k = low; k <= n; k++) src[k] = des[k];
- }
- void mergeSort(int* src, int s, int t)
- {
- if(src == NULL) return ;
- int m;
- if(s < t)
- {
- m = (s+t)/2; //取中間值
- mergeSort(src,s,m); //遞歸地將src[s..m]歸併爲有序的des[s..m]
- mergeSort(src,m+1,t); //遞歸地將src[m+1..t]歸併爲有序的des[m+1..t]
- merge(src,s,m,t);
- }
- }
- int main()
- {
- int src[] = {49,38,65,97,76,13,27,49};
- int i,j,cnt = sizeof(src)/4;
- mergeSort(src,0,cnt-1);
- //輸出
- for(i = 0; i < cnt; i ++) cout<<src[i]<<" ";
- cout<<endl;
- getchar();
- return 0;
- }
五、基數排序
所謂的基數排序 其實就是一種多關鍵字的排序,最經典的例子就是英語字典的編排,先按第一個字母排列,分成26堆,再按第二個字母排列,……以此類推。。。更復雜的基本排序作者本人也沒有研究過,有興趣的朋友可以去網上找相關的資料。
六、各種內部排序的比較
1、時間複雜度達到O(nlgn) 的排序算法有:快速排序、堆排序、歸併排序。
2、上面前四大類排序中,不穩定的排序有:希爾排序、快速排序、堆排序、簡單的選擇排序。
穩定的排序有:插入排序(除希爾外)、冒泡排序、歸併排序。
3、從平均時間性能而言,快速排序最佳,其所需要的時間最少,但快速排序在最壞的情況下,時間性能還不如堆排序和歸併排序。
七、外部排序
外部排序指的是大文件的排序,面試的時候,面試官喜歡問,給你一個非常非常大的文件(比如1T),一行一個數(或者一個單詞),內存最多隻有8G,硬盤足夠大,CPU很高級……然後要你給這個文件裏面的數據排序。你要怎麼辦?
這其實就要用到外部排序。就是說要藉助外存儲器進行多次的內/外存數據的交換,因爲內存不足以加載所有的數據,所以只能一部分一部分地加載。
所以外部排序的思想就是:分兩個獨立的階段。
首先,可按內存的大小,將外存上含n個記錄的文件分成若干長度爲 x 的子文件或段,依次讀入內存,並利用有效的內部排序方法對它們進行排序,並將排序後得到的有序子文件 重新寫入外存,通常稱這些有序的子文件爲歸併段或順串。然後,對這些歸併段進行逐趟歸併,使歸併段逐漸由小到大,直至得到整個有序文件爲止。
因此現在的問題就轉化爲如何歸併兩個大文件。這個讀者朋友們想一下就明白了。就是把這兩個文件按內存的大小,一部分一部分從小到大加載出來並,再寫回外存。
當然歸併的方法有很多種,作者本人也沒怎麼去研究。。。