最近很多同學問我關於排序算法的問題,像冒泡排序,選擇排序。學過數據結構的還好說,對於沒有接觸過數據結構的同學來說內心基本是屬於崩潰的。下面我就來總結一下數據結構中的八大排序算法。
概述
排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。
我們這裏說的八大排序就是內部排序
排序相關的概念
排序(按關鍵字大小順序排列數據)。
排序方法:內部排序,外部排序;簡單的排序方法O(n2),先進的排序方法O(nlogn),基數排序O(dn);插入排序,交換排序,選擇排序,歸併排序,計數排序。
排序方法的穩定性:取決於該方法採取的策略,不是由一次具體的排序結果決定的。但是通過列舉不穩定的排序實例可以說明該排序算法的不穩定性。
1、直接插入排序
直接插入排序是由兩層嵌套循環組成的。外層循環標識並決定待比較的數值。內層循環爲待比較數值確定其最終位置。直接插入排序是將待比較的數值與它的前一個數值進行比較,所以外層循環是從第二個數值開始的。當前一數值比待比較數值大的情況下繼續循環比較,直到找到比待比較數值小的並將待比較數值置入其後一位置,結束該次循環。
將待排序記錄插入已排好的記錄中,不斷擴大有序序列。一句話,“將待排序記錄插入有序序列,重複n-1次” 。
例:排序前: 6 3 3 5 6 3 1 0 6 4
i = 0: 6
i = 1: 3 6
i = 2: 3 3 6
i = 3: 3 3 5 6
i = 4: 3 3 5 6 6
i = 5: 3 3 3 5 6 6
i = 6: 1 3 3 3 5 6 6
i = 7: 0 1 3 3 3 5 6 6
i = 8: 0 1 3 3 3 5 6 6 6
i = 9: 0 1 3 3 3 4 5 6 6 6
排序後: 0 1 3 3 3 4 5 6 6 6
算法實現
void InsertSort(int a[], int n)
{
for(int i= 1; i<n; i++){
if(a[i] < a[i-1]){ //若第i個元素大於i-1元素,直接插入。小於的話,移動有序表後插入
int j= i-1;
int x = a[i]; //複製爲哨兵,即存儲待排序元素
a[i] = a[i-1]; //先後移一個元素
while(x < a[j]){ //查找在有序表的插入位置
a[j+1] = a[j];
j--; //元素後移
}
a[j+1] = x; //插入到正確位置
}
print(a,n,i); //打印每趟排序的結果
}
}
2、希爾排序
希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法,先將待排序列分割成若干個子序列,分別進行直接插入排序,基本有序後再對整個序列進行直接插入排序。
步驟:
1. 分成子序列(按照增量dk);
2. 對子序列排序(直接插入排序);
3. 縮小增量,重複以上步驟,直到增量dk=1。
增量序列中最後一個增量一定是1,如:… 9, 5, 3, 2, 1和… 13, 4, 1。如沒有明確說明增量序列可以選擇… 3, 2, 1或… 5, 3, 2, 1。
算法說明
void ShellSort ( T a[], int n )
{
dk = n/2;
while ( dk>=1 ) {
// 一趟希爾排序,對dk個序列分別進行插入排序
for ( i=dk; i<n; i++ ) {
x = a[i];
for ( j=i-dk; j>=0 and x<a[j]; j-=dk )
a[j+dk] = a[j];
a[j+dk] = x;
}
// 縮小增量
dk = dk/2;
}
}
3、選擇排序
設所排序序列的記錄個數爲n。i取1,2,…,n-1,從所有n-i+1個記錄(Ri,Ri+1,…,Rn)中找出排序碼最小的記錄,與第i個記錄交換。執行n-1趟 後就完成了記錄序列的排序。
第i趟排序過程是在剩餘的待排記錄中選一個最小(大)的,放在第i個位置。
一句話,“在待排記錄中選取最小的,交換到合適位置,重複n-1次” 。
算法實現
void SelectionSort ( T a[], int n )
{
for ( i=0; i<n-1; i++ ) {
k = i;
for ( j=i+1; j<n; j++)
if ( a[j]<a[k] ) k=j; // 最小記錄
if ( k!=i ) a[i]<->a[k];//交換
}
}
簡單選擇排序不愧簡單之名,需要注意的是:
在簡單選擇排序過程中,所需移動記錄的次數比較少。最好情況下,即待排序記錄初始狀態就已經是正序排列了,則不需要移動記錄。
最壞情況下,即待排序記錄初始狀態是按第一條記錄最大,之後的記錄從小到大順序排列,則需要移動記錄的次數最多爲3(n-1)。
時間複雜度O(n2),耗費在比較記錄上,比較次數始終爲n(n-1)/2,移動次數最小爲0,最大3(n-1),即n-1次交換。
注意:簡單選擇排序是不穩定的。
4、冒泡排序
冒泡排序算法的運作如下:(從後往前)
比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。針對所有的元素重複以上的步驟,除了最後一個。持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
算法實現
void bubbleSort(int a[], int n){
for(int i =0 ; i< n-1; ++i) {
for(int j = 0; j < n-i-1; ++j) {
if(a[j] > a[j+1])
{
int tmp = a[j] ;
a[j] = a[j+1] ;
a[j+1] = tmp;
}
}
}
}
對冒泡排序常見的改進方法是加入一標誌性變量,用於標誌某一趟排序過程中是否有數據交換,如果進行某一趟排序時並沒有進行數據交換,則說明數據已經按要求排列好,可立即結束排序,避免不必要的比較過程。本文再提供以下兩種改進算法:
1.設置一標誌性變量pos,用於記錄每趟排序中最後一次進行交換的位置。由於pos位置之後的記錄均已交換到位,故在進行下一趟排序時只要掃描到pos位置即可。
改進後算法如下:
void Bubble_1 ( int r[], int n) {
int i= n -1; //初始時,最後位置保持不變
while ( i> 0) {
int pos= 0; //每趟開始時,無記錄交換
for (int j= 0; j< i; j++)
if (r[j]> r[j+1]) {
pos= j; //記錄交換的位置
int tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;
}
i= pos; //爲下一趟排序作準備
}
}
2.傳統冒泡排序中每一趟排序操作只能找到一個最大值或最小值,我們考慮利用在每趟排序中進行正向和反向兩遍冒泡的方法一次可以得到兩個最終值(最大者和最小者) , 從而使排序趟數幾乎減少了一半。可以參考折半查找法
改進後的算法實現爲:
void Bubble_2 ( int r[], int n){
int low = 0;
int high= n -1; //設置變量的初始值
int tmp,j;
while (low < high) {
for (j= low; j< high; ++j) //正向冒泡,找到最大者
if (r[j]> r[j+1]) {
tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;
}
--high; //修改high值, 前移一位
for ( j=high; j>low; --j) //反向冒泡,找到最小者
if (r[j]<r[j-1]) {
tmp = r[j]; r[j]=r[j-1];r[j-1]=tmp;
}
++low; //修改low值,後移一位
}
}
好,今天先介紹這麼多,剩下的改天再進行介紹。
剩下的還有快速排序、堆排序、歸併排序、基數排序,有時間我們接着聊。
參考資料
- 嚴蔚敏,吳偉民.數據結構(c語言版):清華大學出版社,1997.4
- 數據結構輔導講義—莊波
- 百度百科