氣泡排序、插入排序、選擇排序、希爾排序、堆排序、歸併排序等常用排序演算法的比較
掌握好常用的排序演算法,在實際的專案開發中可以節省很多的時間。每一種排序演算法在執行的效率上是存在差別的,這些微小的時間差,也許在平常的聯絡當中感覺不到,但是涉及到資料量比較大或者是在資源比較緊張的系統中就顯得尤其的重要,比如嵌入式系統。下面簡要介紹三種常用的排序演算法以及他們的執行效率的比較。
氣泡排序:最優為O(n),最壞為O(n^2),平均O(n^2)
思路:將相鄰的兩個數比較,將較小的數調到前頭;有n個數就要進行n-1趟比較,第一次比較中要進行n-1次兩兩比較,在第j趟比較中,要進行n-j次兩兩比較。
實現程式碼:
為什麼說冒泡法最佳時間複雜度為O(N)呢?因為冒泡法的演算法得到了優化,如下:<span style="font-size:14px;"> void BublleSort (int arr [], int count) { int i, j, temp; for(j=0; j<count-1; j++ ) /* 冒泡法要排序n-1次*/ for(i=0; i<count-j-1; i++ )/* 值比較大的元素沉下去後,只把剩下的元素中的最大值再沉下去就可以啦 */ { if(arr[i]>arr[i+1])/* 把值比較大的元素沉到底 */ { temp=arr[i+1]; arr[i+1]=arr[i]; arr[i]=temp; } } }</span>
public void bubbleSort(int arr[]) { boolean didSwap; for(int i = 0, len = arr.length; i < len - 1; i++) { didSwap = false; for(int j = 0; j < len - i - 1; j++) { if(arr[j + 1] < arr[j]) { swap(arr, j, j + 1); didSwap = true; } } if(didSwap == false) return; } }
插入排序:最優為O(n),最壞為O(n^2),平均O(n^2)
思路:在得到要排序的陣列以後,講陣列分為兩個部分,陣列的第一個元素為一個部分,剩下的元素為一部分,然後從陣列的第二個元素開始,和該元素以前的所有元素比較,如果之前的元素沒有比該元素大的,那麼該元素的位置不變,如果有元素的值比該元素大,那麼記錄他所在的位置;例如I,該元素的位置為k,則將從i到k位置上的所有元素往後移動一位,然後將k位置上的值移動到i位置上。這樣就找到了K所在的位置。每一個元素都這樣進行,最終就會得到排好順序的陣列。
實現程式碼:
void InsertSort ( int arr[],int count) { int i,j,temp; for(i=1; i<count; i )//陣列分兩個部分,從第二個陣列元素開始 { temp = arr[i];//操作當前元素,先儲存在其它變數中 for(j=i-1; j>=0 && arr[j]>temp; j--)//從當前元素的上一個元素開始查詢合適的位置,一直查詢到首元素,如果大則後移 { arr[ j + 1 ] = arr[ j ]; } arr[ j + 1 ] = temp; //將當前元素放置在合適位置 } }
選擇排序:優為O(nlogn),最壞為O(n^2),平均O(nlogn)
思路:
首先以一個元素為基準,從一個方向開始掃描,比如從左到右掃描,以A[0]為基準,接下來從A[0]….A[9]中找出最小的元素,將其與A[0]交換。然後將其基準位置右移一位,重複上面的動作,比如,以A[1]為基準,找出A[1]~A[9]中最小的,將其與A[1]交換。一直進行到將基準位置移到陣列最後一個元素時排序結束。
實現程式碼:
void selectsort(int arr[], int n)
{
int i = 0, j = 0, iindex = 0;
int temp = 0;
int iMin = 0;
for (i = 0; i < n;i++)
{
iMin = arr[i];
iindex = i; //未交換則不改變
for (j = i + 1; j < n;j++)
{
if (arr[j] < iMin)
{
iMin = arr[j]; //儲存最小值
iindex = j; //儲存最小值索引
}
}
if ( i != iindex )
{
temp = arr[iindex];
arr[iindex] = arr[i]; //最小值交換
arr[i] = temp;
}
}
}
效率比較:
為了能夠更加明顯的檢視其效果,將每個排序演算法執行10000次。下面是測試程式主函式:
#include <stdio.h>
#include<stdlib.h>
#include <sys/time.h>
#include <unistd.h>
#define MAX 6
int array[MAX];
int count = MAX;
/********建立陣列,並輸入元素************/
void BuildArray()
{
int a,i=0;
printf("請輸入陣列元素: ");
for(; i<count; i )
{
scanf("%d", &a);
array[i] = a;
}
printf("\n");
}
/**********遍歷輸出陣列元素*************/
void Traverse(int arr[], int count)
{
int i;
printf("陣列輸出: ");
for(i=0; i<count; i )
printf("%d\t", arr[i]);
printf("\n");
}
void BublleSort(int arr[], int count)
{
int i,j,temp;
for(j=0; j<count-1; j ) /* 氣泡法要排序n-1次*/
for(i=0; i<count-j-1; i )/* 值比較大的元素沉下去後,只把剩下的元素中的最大值再沉下去就可以啦 */
{
if(arr[i]>arr[i 1])/* 把值比較大的元素沉到底 */
{
temp=arr[i 1];
arr[i 1]=arr[i];
arr[i]=temp;
}
}
}
void InsertSort(int arr[],int count)
{
int i,j,temp;
for(i=1; i<count; i )//陣列分兩個部分,從第二個陣列元素開始
{
temp = arr[i];//操作當前元素,先儲存在其它變數中
for(j=i-1; j>-1&&arr[j]>temp;j--)//從當前元素的上一個元素開始查詢合適的位置,一直查詢到首元素
{
arr[i] = arr[j];
arr[j] = temp;
}
}
}
void SelectSort(int arr[], int count)
{
int i,j,min,temp;
for(i=0; i<count; i )
{
min = arr[i];//以此元素為基準
for(j=i 1; j<count; j )//從j往前的資料都是排好的,所以從j開始往下找剩下的元素中最小的
{
if(min>arr[j])//把剩下元素中最小的那個放到arr[j]中
{
temp = arr[j];
arr[j] = min;
min = temp;
}
}
}
}
int main()
{
int i;
struct timeval tv1,tv2;
struct timezone tz;
BuildArray();//建立陣列
Traverse(array, count);//輸出最初陣列
gettimeofday(&tv1,&tz);
for(i=0;i<10000;i++)
BublleSort(array, count);//氣泡排序
gettimeofday(&tv2,&tz);
printf("%d:%d/n",tv2.tv_sec-tv1.tv_sec,tv2.tv_usec-tv1.tv_usec);
Traverse(array, count);//輸出排序後的陣列
gettimeofday(&tv1,&tz);
for(i=0;i<10000;i++)
InsertSort(array, count);//插入排序
gettimeofday(&tv2,&tz);
printf("%d:%d/n",tv2.tv_sec-tv1.tv_sec,tv2.tv_usec-tv1.tv_usec);
Traverse(array, count);//輸出排序後的陣列
gettimeofday(&tv1,&tz);
for(i=0;i<10000;i++)
SelectSort(array, count);//插入排序
gettimeofday (&tv2,&tz);
printf("%d:%d/n",tv2.tv_sec-tv1.tv_sec,tv2.tv_usec-tv1.tv_usec);
Traverse(array, count);//輸出排序後的陣列
return 0;
}
編譯:gcc –g –Wall sort_test.c –o sort_test
執行:./sort_test
結果如下:
通過多次測試,插入排序的速度最快。
希爾排序:
希爾排序的實質就是分組插入排序,該方法又稱縮小增量排序,因DL.Shell於1959年提出而得名。該方法的基本思想是:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。因為直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率上比前兩種方法有較大提高。
以n=10的一個數組49, 38, 65, 97, 26, 13, 27, 49, 55, 4為例
第一次 gap = 10 / 2 = 5
49 38 65 97 26 13 27 49 55 4
1A 1B
2A 2B
3A 3B
4A 4B
5A 5B
1A,1B,2A,2B等為分組標記,數字相同的表示在同一組,大寫字母表示是該組的第幾個元素, 每次對同一組的資料進行直接插入排序。即分成了五組(49, 13) (38, 27) (65, 49) (97, 55) (26, 4)這樣每組排序後就變成了(13, 49) (27, 38) (49, 65) (55, 97) (4, 26),下同。
第二次 gap = 5 / 2 = 2
排序後
13 27 49 55 4 49 38 65 97 26
1A 1B 1C 1D 1E
2A 2B 2C 2D 2E
第三次 gap = 2 / 2 = 1
4 26 13 27 38 49 49 55 97 65
1A 1B 1C 1D 1E 1F 1G 1H 1I 1J
第四次 gap = 1 / 2 = 0 排序完成得到陣列:
4 13 26 27 38 49 49 55 65 97
下面給出嚴格按照定義來寫的希爾排序
void shellsort1(int a[], int n)
{
int i, j, gap;
for (gap = n / 2; gap > 0; gap /= 2) //步長
for (i = 0; i < gap; i++) //直接插入排序
{
for (j = i + gap; j < n; j += gap)
if (a[j] < a[j - gap])
{
int temp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > temp)
{
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = temp;
}
}
}
很明顯,上面的shellsort1程式碼雖然對直觀的理解希爾排序有幫助,但程式碼量太大了,不夠簡潔清晰。因此進行下改進和優化,以第二次排序為例,原來是每次從1A到1E,從2A到2E,可以改成從1B開始,先和1A比較,然後取2B與2A比較,再取1C與前面自己組內的資料比較…….。這種每次從陣列第gap個元素開始,每個元素與自己組內的資料進行直接插入排序顯然也是正確的。
void shellsort2(int a[], int n)
{
int j, gap;
for (gap = n / 2; gap > 0; gap /= 2)
for (j = gap; j < n; j++)//從陣列第gap個元素開始
if (a[j] < a[j - gap])//每個元素與自己組內的資料進行直接插入排序
{
int temp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > temp)
{
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = temp;
}
}
堆排序
堆排序與快速排序,歸併排序一樣都是時間複雜度為O(N*logN)的幾種常見排序方法。學習堆排序前,先講解下什麼是資料結構中的二叉堆。二叉堆的定義
二叉堆是完全二叉樹或者是近似完全二叉樹。
二叉堆滿足二個特性:
1.父結點的鍵值總是大於或等於(小於或等於)任何一個子節點的鍵值。
2.每個結點的左子樹和右子樹都是一個二叉堆(都是最大堆或最小堆)。
當父結點的鍵值總是大於或等於任何一個子節點的鍵值時為最大堆。當父結點的鍵值總是小於或等於任何一個子節點的鍵值時為最小堆。下圖展示一個最小堆:
由於其它幾種堆(二項式堆,斐波納契堆等)用的較少,一般將二叉堆就簡稱為堆。
堆的儲存
一般都用陣列來表示堆,i結點的父結點下標就為(i – 1) / 2。它的左右子結點下標分別為2 * i + 1和2 * i + 2。如第0個結點左右子結點下標分別為1和2。
堆的操作——插入刪除
下面先給出《資料結構C++語言描述》中最小堆的建立插入刪除的圖解,再給出本人的實現程式碼,最好是先看明白圖後再去看程式碼。
堆的插入
每次插入都是將新資料放在陣列最後。可以發現從這個新資料的父結點到根結點必然為一個有序的數列,現在的任務是將這個新資料插入到這個有序資料中——這就類似於直接插入排序中將一個數據併入到有序區間中,對照《白話經典算法系列之二 直接插入排序的三種實現》不難寫出插入一個新資料時堆的調整程式碼:
// 新加入i結點 其父結點為(i - 1) / 2void MinHeapFixup(int a[], int i)
{
int j, temp;
temp = a[i];
j = (i - 1) / 2; //父結點
while (j >= 0 && i != 0)
{
if (a[j] <= temp)
break;
a[i] = a[j]; //把較大的子結點往下移動,替換它的子結點
i = j;
j = (i - 1) / 2;
}
a[i] = temp;
}
更簡短的表達為:
void MinHeapFixup(int a[], int i)
{
for (int j = (i - 1) / 2; (j >= 0 && i != 0)&& a[i] > a[j]; i = j, j = (i - 1) / 2)
Swap(a[i], a[j]);
}
插入時:
//在最小堆中加入新的資料nNumvoid MinHeapAddNumber(int a[], int n, int nNum)
{
a[n] = nNum;
MinHeapFixup(a, n);
}
堆的刪除
按定義,堆中每次都只能刪除第0個數據。為了便於重建堆,實際的操作是將最後一個數據的值賦給根結點,然後再從根結點開始進行一次從上向下的調整。調整時先在左右兒子結點中找最小的,如果父結點比這個最小的子結點還小說明不需要調整了,反之將父結點和它交換後再考慮後面的結點。相當於從根結點將一個數據的“下沉”過程。下面給出程式碼:
// 從i節點開始調整,n為節點總數 從0開始計算 i節點的子節點為 2*i+1, 2*i+2void MinHeapFixdown(int a[], int i, int n)
{
int j, temp;
temp = a[i];
j = 2 * i + 1;
while (j < n)
{
if (j + 1 < n && a[j + 1] < a[j]) //在左右孩子中找最小的
j++;
if (a[j] >= temp)
break;
a[i] = a[j]; //把較小的子結點往上移動,替換它的父結點
i = j;
j = 2 * i + 1;
}
a[i] = temp;
}
//在最小堆中刪除數
void MinHeapDeleteNumber(int a[], int n)
{
Swap(a[0], a[n - 1]);
MinHeapFixdown(a, 0, n - 1);
}
堆化陣列
有了堆的插入和刪除後,再考慮下如何對一個數據進行堆化操作。
寫出堆化陣列的程式碼:
//建立最小堆void MakeMinHeap(int a[], int n)
{
for (int i = n / 2 - 1; i >= 0; i--)
MinHeapFixdown(a, i, n);
}
至此,堆的操作就全部完成了(注1),再來看下如何用堆這種資料結構來進行排序。
堆排序
首先可以看到堆建好之後堆中第0個數據是堆中最小的資料。取出這個資料再執行下堆的刪除操作。這樣堆中第0個數據又是堆中最小的資料,重複上述步驟直至堆中只有一個數據時就直接取出這個資料。
由於堆也是用陣列模擬的,故堆化陣列後,第一次將A[0]與A[n - 1]交換,再對A[0…n-2]重新恢復堆。第二次將A[0]與A[n – 2]交換,再對A[0…n - 3]重新恢復堆,重複這樣的操作直到A[0]與A[1]交換。由於每次都是將最小的資料併入到後面的有序區間,故操作完成後整個陣列就有序了。有點類似於。
void MinheapsortTodescendarray(int a[], int n)
{
for (int i = n - 1; i >= 1; i--)
{
Swap(a[i], a[0]);
MinHeapFixdown(a, 0, i);
}
}
注意使用最小堆排序後是遞減陣列,要得到遞增陣列,可以使用最大堆。
由於每次重新恢復堆的時間複雜度為O(logN),共N - 1次重新恢復堆操作,再加上前面建立堆時N / 2次向下調整,每次調整時間複雜度也為O(logN)。二次操作時間相加還是O(N * logN)。故堆排序的時間複雜度為O(N * logN)。STL也實現了堆的相關函式,可以參閱《STL系列之四 heap 堆》。
注1 作為一個數據結構,最好用類將其資料和方法封裝起來,這樣即便於操作,也便於理解。此外,除了堆排序要使用堆,另外還有很多場合可以使用堆來方便和高效的處理資料,以後會一一介紹。
再貼一份原始碼: Heap Sort//堆篩選函式
//已知H[start~end]中除了start之外均滿足堆的定義
//本函式進行調整,使H[start~end]成為一個大頂堆
typedef int ElemType;
void HeapAdjust(ElemType H[], int start, int end)
{
ElemType temp = H[start];
for(int i = 2*start + 1; i<=end; i*=2)
{
//因為假設根結點的序號為0而不是1,所以i結點左孩子和右孩子分別為2i+1和2i+2
if(i<end && H[i]<H[i+1])//左右孩子的比較
{
++i;//i為較大的記錄的下標
}
if(temp > H[i])//左右孩子中獲勝者與父親的比較
{
break;
}
//將孩子結點上位,則以孩子結點的位置進行下一輪的篩選
H[start]= H[i];
start = i;
}
H[start]= temp; //插入最開始不和諧的元素
}
void HeapSort(ElemType A[], int n)
{
//先建立大頂堆
for(int i=n/2; i>=0; --i)
{
HeapAdjust(A,i,n);
}
//進行排序
for(int i=n-1; i>0; --i)
{
//最後一個元素和第一元素進行交換
ElemType temp=A[i];
A[i] = A[0];
A[0] = temp;
//然後將剩下的無序元素繼續調整為大頂堆
HeapAdjust(A,0,i-1);
}
}
歸併排序
歸併排序是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路歸併。 歸併操作的工作原理如下:- 第一步:申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
- 第二步:設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
- 第三步:比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
- 重複步驟3直到某一指標超出序列尾
- 將另一序列剩下的所有元素直接複製到合併序列尾
#define N 8
void merge(int x[], int low,int mid,int high) /* 對子序列x[low~mid]和x[mid+1~high]進行歸併操作 */
{
int i, j, k;
int m, n;
int t;
int y[N]; /* 臨時緩衝區 */
i = low; /* 指向前一個子序列的起始位置 */
j = mid+1; /* 指向後一個子序列的起始位置 */
for(k=low; i<=mid&&j<=high; k++) /* 逐個比較兩個子序列中資料元素的大小 */
{
if(x[i] <= x[j]) /* 將較小的資料元素放入緩衝區 */
y[k] = x[i++];
else
y[k] = x[j++];
}
/* 如果前一個子序列中的資料元素已經比較完畢,則直接複製後一個子序列中的資料元素到緩衝區 */
if(i <= mid)
{
for(m=i; m<=mid; m++)
{
y[k++] = x[m];
}
}
/* 如果後一個子序列中的資料元素已經比較完畢,則直接複製前一個子序列中的資料元素到緩衝區 */
if(j <= high)
{
for(n=j; n<=high; n++)
{
y[k++] = x[n];
}
}
/* 將緩衝區中的資料元素複製回原序列中 */
for (t=low; t<=high; t++)
{
x[t] = y[t];
}
}
void me_sort(int x[], int low,int high) /* 定義歸併排序函式,遞迴方式 */
{
int mid;
if(low < high)
{
mid = (low+high)/2;
me_sort(x, low, mid); /* 遞迴呼叫,將子序列x[low~mid]歸併為有序序列 */
me_sort(x, mid+1, high); /* 遞迴呼叫,將子序列x[mid+1~high]歸併為有序序列 */
merge(x, low,mid,high); /* 將子序列x[low~mid]和x[mid+1~high]進行歸併 */
}
}
再附上一份程式碼,來自百度知道:
#include<stdlib.h>
#include<stdio.h>
void Merge(int sourceArr[],int tempArr[],int startIndex,int midIndex,int endIndex)
{
int i = startIndex,j=midIndex+1,k = startIndex;
while(i!=midIndex+1 && j!=endIndex+1)
{
if(sourceArr[i]>sourceArr[j])
tempArr[k++] = sourceArr[i++];
else
tempArr[k++] = sourceArr[j++];
}
while(i!=midIndex+1)
tempArr[k++] = sourceArr[i++];
while(j!=endIndex+1)
tempArr[k++] = sourceArr[j++];
for(i=startIndex;i<=endIndex;i++)
sourceArr[i] = tempArr[i];
}
//內部使用遞迴
void MergeSort(int sourceArr[],int tempArr[],int startIndex,int endIndex)
{
int midIndex;
if(startIndex<endIndex)
{
midIndex=(startIndex+endIndex)/2;
MergeSort(sourceArr,tempArr,startIndex,midIndex);
MergeSort(sourceArr,tempArr,midIndex+1,endIndex);
Merge(sourceArr,tempArr,startIndex,midIndex,endIndex);
}
}
int main(int argc,char * argv[])
{
int a[8]={50,10,20,30,70,40,80,60};
int i,b[8];
MergeSort(a,b,0,7);
for(i=0;i<8;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
快速排序
快速排序(Quicksort)是對氣泡排序的一種改進。快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。 設要排序的陣列是A[0]……A[N-1],首先任意選取一個數據(通常選用陣列的第一個數)作為關鍵資料,然後將所有比它小的數都放到它前面,所有比它大的數都放到它後面,這個過程稱為一趟快速排序。值得注意的是,快速排序不是一種穩定的排序演算法,也就是說,多個相同的值的相對位置也許會在演算法結束時產生變動。 一趟快速排序的演算法是:- 1)設定兩個變數i、j,排序開始的時候:i=0,j=N-1;
- 2)以第一個陣列元素作為關鍵資料,賦值給key,即key=A[0];
- 3)從j開始向前搜尋,即由後開始向前搜尋(j--),找到第一個小於key的值A[j],將A[j]和A[i]互換;
- 4)從i開始向後搜尋,即由前開始向後搜尋(i++),找到第一個大於key的A[i],將A[i]和A[j]互換;
- 5)重複第3、4步,直到i=j; (3,4步中,沒找到符合條件的值,即3中A[j]不小於key,4中A[i]不大於key的時候改變j、i的值,使得j=j-1,i=i+1,直至找到為止。找到符合條件的值,進行交換的時候i, j指標位置不變。另外,i==j這一過程一定正好是i+或j-完成的時候,此時令迴圈結束)。
#include <iostream>
using namespace std;
void Qsort(int a[], int low, int high)
{
if(low >= high)
{
return;
}
int first = low;
int last = high;
int key = a[first];/*用字表的第一個記錄作為樞軸*/
while(first < last)
{
while(first < last && a[last] >= key)
{
--last;
}
a[first] = a[last];/*將比第一個小的移到低端*/
while(first < last && a[first] <= key)
{
++first;
}
a[last] = a[first];
/*將比第一個大的移到高階*/
}
a[first] = key;/*樞軸記錄到位*/
Qsort(a, low, first-1);
Qsort(a, first+1, high);
}
int main()
{
int a[] = {57, 68, 59, 52, 72, 28, 96, 33, 24};
Qsort(a, 0, sizeof(a) / sizeof(a[0]) - 1);/*這裡原文第三個引數要減1否則記憶體洩露*/
for(int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
cout << a[i] << "";
}
return 0;
}/*參考資料結構p274(清華大學出版社,嚴蔚敏)*/
再貼一段程式碼:
void sort(int *a, int left, int right)
{
if(left >= right)/*如果左邊的陣列大於或者等於就代表已經整理完成一個組了*/
{
return ;
}
int i = left;
int j = right;
int key = a[left];
while(i < j) /*控制在當組內尋找一遍*/
{
while(i < j && key <= a[j])
/*而尋找結束的條件就是,1,找到一個小余或者大於key的數(大小取決於你想升
序還是降序)2,沒有符合的切i與j相遇*/
{
j--;/*向前尋找*/
}
a[i] = a[j];
/*找到一個這樣的數後就把它賦給前面的被拿走的i的值(如果第一次迴圈且key是
a[0],那麼就是給key)*/
while(i < j && key >= a[i])
/*這是i在當組內向前尋找,同上,不過注意與key的大小關係停止迴圈和上面相反,
因為排序思想是把數往兩邊扔,所以左右兩邊的數大小與key的關係相反*/
{
i++;
}
a[j] = a[i];
}
a[i] = key;/*當在當組內找完一遍以後就把中間數key迴歸*/
sort(a, left, i - 1);/*最後用同樣的方式對分出來的左邊的小組進行同上的做法*/
sort(a, i + 1, right);/*用同樣的方式對分出來的右邊的小組進行同上的做法*/
/*當然最後可能會出現很多分左右,直到每一組的i = j 為止*/
}
排序演算法穩定性:
這幾天筆試了好幾次了,連續碰到一個關於常見排序演算法穩定性判別的問題,往往還是多選,對於我以及和我一樣拿不準的同學可不是一個能輕易下結論的題目,當然如果你筆試之前已經記住了資料結構書上哪些是穩定的,哪些不是穩定的,做起來應該可以輕鬆搞定。
本文是針對老是記不住這個或者想真正明白到底為什麼是穩定或者不穩定的人準備的。
首先,排序演算法的穩定性大家應該都知道,通俗地講就是能保證排序前2個相等的數其在序列的前後位置順序和排序後它們兩個的前後位置順序相同。在簡單形式化一下,如果Ai = Aj, Ai原來在位置前,排序後Ai還是要在Aj位置前。
其次,說一下穩定性的好處。排序演算法如果是穩定的,那麼從一個鍵上排序,然後再從另一個鍵上排序,第一個鍵排序的結果可以為第二個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位相同的元素其順序再高位也相同時是不會改變的。另外,如果排序演算法穩定,對基於比較的排序演算法而言,元素交換的次數可能會少一些(個人感覺,沒有證實)。
回到主題,現在分析一下常見的排序演算法的穩定性,每個都給出簡單的理由。
(1)氣泡排序
氣泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,我想你是不會再無聊地把他們倆交換一下的;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序並沒有改變,所以氣泡排序是一種穩定排序演算法。
(2)選擇排序
選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩餘元素裡面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因為只剩下它一個最大的元素了。那麼,在一趟選擇,如果當前元素比一個元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9, 我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對前後順序就被破壞了,所以選擇排序不是一個穩定的排序演算法。
(3)插入排序
插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其後面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的。
(4)快速排序
快速排序有兩個方向,左邊的i下標一直往右走,當a[i] <= a[center_index],其中center_index是中樞元素的陣列下標,一般取為陣列第0個元素。而右邊的j下標一直往左走,當a[j] > a[center_index]。如果i和j都走不動了,i <= j, 交換a[i]和a[j],重複上面的過程,直到i>j。 交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交換的時候,很有可能把前面的元素的穩定性打亂,比如序列為
5 3 3 4 3 8 9 10 11, 現在中樞元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂,所以快速排序是一個不穩定的排序演算法,不穩定發生在中樞元素和a[j]交換的時刻。
(5)歸併排序
歸併排序是把序列遞迴地分成短序列,遞迴出口是短序列只有1個元素(認為直接有序)或者2個序列(1次比較和交換),然後把各個有序的段序列合併成一個有序的長序列,不斷合併直到原序列全部排好序。可以發現,在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩定性。那麼,在短的有序序列合併的過程中,穩定是是否受到破壞?沒有,合併過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素儲存在結果序列的前面,這樣就保證了穩定性。所以,歸併排序也是穩定的排序演算法。
(6)基數排序
基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先順序排序,最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。基數排序基於分別排序,分別收集,所以其是穩定的排序演算法。
(7)希爾排序(shell)
希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小,插入排序對於有序的序列效率很高。所以,希爾排序的時間複雜度會比o(n^2)好一些。由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。
(8)堆排序
我們知道堆的結構是節點i的孩子為2*i和2*i+1節點,大頂堆要求父節點大於等於其2個子節點,小頂堆要求父節點小於等於其2個子節點。在一個長為n的序列,堆排序的過程是從第n/2開始和其子節點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇當然不會破壞穩定性。但當為n/2-1, n/2-2, ...1這些個父節點選擇元素時,就會破壞穩定性。有可能第n/2個父節點交換把後面一個元素交換過去了,而第n/2-1個父節點把後面一個相同的元素沒有交換,那麼這2個相同的元素之間的穩定性就被破壞了。所以,堆排序不是穩定的排序演算法
1 快速排序(QuickSort)
快速排序是一個就地排序,分而治之,大規模遞迴的演算法。從本質上來說,它是歸併排序的就地版本。快速排序可以由下面四步組成。
(1) 如果不多於1個數據,直接返回。
(2) 一般選擇序列最左邊的值作為支點資料。
(3) 將序列分成2部分,一部分都大於支點資料,另外一部分都小於支點資料。
(4) 對兩邊利用遞迴排序數列。
快速排序比大部分排序演算法都要快。儘管我們可以在某些特殊的情況下寫出比快速排序快的演算法,但是就通常情況而言,沒有比它更快的了。快速排序是遞迴的,對於記憶體非常有限的機器來說,它不是一個好的選擇。
2 歸併排序(MergeSort)
歸併排序先分解要排序的序列,從1分成2,2分成4,依次分解,當分解到只有1個一組的時候,就可以排序這些分組,然後依次合併回原來的序列中,這樣就可以排序所有資料。合併排序比堆排序稍微快一點,但是需要比堆排序多一倍的記憶體空間,因為它需要一個額外的陣列。
3 堆排序(HeapSort)
堆排序適合於資料量非常大的場合(百萬資料)。
堆排序不需要大量的遞迴或者多維的暫存陣列。這對於資料量非常巨大的序列是合適的。比如超過數百萬條記錄,因為快速排序,歸併排序都使用遞迴來設計演算法,在資料量非常大的時候,可能會發生堆疊溢位錯誤。
堆排序會將所有的資料建成一個堆,最大的資料在堆頂,然後將堆頂資料和序列的最後一個數據交換。接下來再次重建堆,交換資料,依次下去,就可以排序所有的資料。
4 Shell排序(ShellSort)
Shell排序通過將資料分成不同的組,先對每一組進行排序,然後再對所有的元素進行一次插入排序,以減少資料交換和移動的次數。平均效率是O(nlogn)。其中分組的合理性會對演算法產生重要的影響。現在多用D.E.Knuth的分組方法。
Shell排序比氣泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相對比較簡單,它適合於資料量在5000以下並且速度並不是特別重要的場合。它對於資料量較小的數列重複排序是非常好的。
5 插入排序(InsertSort)
插入排序通過把序列中的值插入一個已經排序好的序列中,直到該序列的結束。插入排序是對氣泡排序的改進。它比氣泡排序快2倍。一般不用在資料大於1000的場合下使用插入排序,或者重複排序超過200資料項的序列。
6 氣泡排序(BubbleSort)
氣泡排序是最慢的排序演算法。在實際運用中它是效率最低的演算法。它通過一趟又一趟地比較陣列中的每一個元素,使較大的資料下沉,較小的資料上升。它是O(n^2)的演算法。
7 交換排序(ExchangeSort)和選擇排序(SelectSort)
這兩種排序方法都是交換方法的排序演算法,效率都是 O(n2)。在實際應用中處於和氣泡排序基本相同的地位。它們只是排序演算法發展的初級階段,在實際中使用較少。
8 基數排序(RadixSort)
基數排序和通常的排序演算法並不走同樣的路線。它是一種比較新穎的演算法,但是它只能用於整數的排序,如果我們要把同樣的辦法運用到浮點數上,我們必須瞭解浮點數的儲存格式,並通過特殊的方式將浮點數對映到整數上,然後再映射回去,這是非常麻煩的事情,因此,它的使用同樣也不多。而且,最重要的是,這樣演算法也需要較多的儲存空間。
9 總結
下面是一個總的表格,大致總結了我們常見的所有的排序演算法的特點。
排序法 | 平均時間 | 最差情形 | 穩定度 | 額外空間 | 備註 |
冒泡 | O(n2) | O(n2) | 穩定 | O(1) | n小時較好 |
交換 | O(n2) | O(n2) | 不穩定 | O(1) | n小時較好 |
選擇 | O(n2) | O(n2) | 不穩定 | O(1) | n小時較好 |
插入 | O(n2) | O(n2) | 穩定 | O(1) | 大部分已排序時較好 |
基數 | O(logRB) | O(logRB) | 穩定 | O(n) |
B是真數(0-9), R是基數(個十百) |
Shell | O(nlogn) | O(ns) 1<s<2 | 不穩定 | O(1) | s是所選分組 |
快速 | O(nlogn) | O(n2) | 不穩定 | O(logn) | n大時較好 |
歸併 | O(nlogn) | O(nlogn) | 穩定 | O(n) | n大時較好 |
堆 | O(nlogn) | O(nlogn) | 不穩定 | O(1) | n大時較好 |