寫在前面:
排序是計算機程序設計中的一種重要操作,它的功能是將一個數據元素的任意序列,重新排列成一個按關鍵字有序的序列。因此排序掌握各種排序算法非常重要。對下面介紹的各個排序,我們假定所有排序的關鍵字都是整數、對傳入函數的參數默認是已經檢查好了的。只是簡單的描述各個算法並給出了具體實現代碼,並未做其他深究探討。
基礎知識:
由於待排序的記錄數量不同,使得排序過程中設計的存儲器不同,可將排序方法分爲兩大類:一類是內部排序,指的是待排序記錄存放在計算機隨機存儲器中進行的排序過程。另一類是外部排序,指的是待排序記錄的數量很大,以致內存一次不能容納全部記錄,在排序過程中尚需對外存進行訪問的排序過程。
在排序的過程中需進行下列兩種基本操作:1、比較兩個關鍵字的大小;2、將記錄從一個位置移動至另一個位置。操作1對大多數排序方法來說都是必要的,而操作2可以通過改變記錄的存儲方式予以避免。
待排序記錄序列可有下列3種存儲方式:1、待排序的一組記錄存放在地址連續的一組存儲單元上。2、一組待排序記錄存放在靜態鏈表中,記錄之間的次序關係由指針指示,則實現不需要移動記錄,僅需要修改指針即可。3、待排序記錄本身存儲在一組地址連續的存儲單元內,同時另設一個指示各個記錄存儲位置的地址向量,在排序過程中不移動記錄本身,而移動地址向量中這些記錄的”地址“,在排序結束之後再按照地址向量中的值調整記錄的存儲位置。
算法分析:
1、插入排序:
基本思想:將一個記錄插入到已排好序的有序表中,從而得到一個新的、記錄數增1的有序表。
時間複雜度爲O(n^2),若待排記錄序列爲正序,時間複雜度可提高至O(n);空間上只需要一個記錄的輔助空間。
a、直接插入排序
示例代碼1:
- void InsertionSort(ElementType A[], int N)
- {
- int j, P;
- ElementType Tmp;//記錄輔助空間
- for(P = 1; P < N; P++){
- Tmp = A[P];
- for(j = P; j > 0 && A[j - 1] > Tmp; j--)//將一個記錄插入已排好序的有序表中
- A[j] = A[j - 1];
- A[j] = Tmp;
- }
- }
示例代碼2:
- void insertionsort(ElementType A[], int N)
- {
- for(int i = 1; i < N; i++){
- int tmp = A[i]; //記錄輔助空間
- int j = i - 1;
- while(j > -1 && A[j] > A[i]){
- A[j+1] = A[j]; //將一個記錄插入已排好序的有序表中
- --j;
- }
- A[j+1] = tmp;
- }
- return;
- }
插入排序算法簡單,且容易實現。當待排序記錄的數量n很小時,這是一種很好的排序方法。但n很大時,則不宜採用直接排序。因爲直接排序,主要的時間消耗在“比較”和“移動”上,因此,在直接排序的基礎上,從減少“比較”和“移動”這兩種操作的次數着眼,可得“折半插入排序”、“2-路插入排序”、“表插入排序”等。
b、折半插入排序
由於插入排序的基本操作是一個有序表中進行查找和插入,這個"查找"操作可利用"折半查找"來實現,由此進行的插入排序稱之爲折半插入排序。
2、希爾排序
基本思想:先將整個待排記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行一次直接插入排序。可以看出希爾排序其實只是改進了的插入排序,因此上面的插入排序也被稱爲直接插入排序。
特點:子序列的構成不是簡單地“逐段分割”,而是將相隔某個“增量”的記錄組成一個子序列。它通過比較相距一定間隔的元素來工作;各趟比較所用的距離隨着算法的進行而減小,直到只比較相鄰元素的最後一趟排序爲止。
示例代碼
- void Shellsort(ElementType A[], int N)
- {
- int i, j, Increment;
- ElementType Tmp;
- for(Increment = N / 2; Increment > 0; Increment /= 2){
- for(i = Increment; i < N; i++){
- Tmp = A[i];
- for(j = i; j >= Increment; j -= Increment){
- if(Tmp < A[j - Increment])
- A[j] = A[j - Increment];
- else
- break;
- }
- A[j] = Tmp;
- }
- }
- }
上面給出的示例中選擇的排序增量是使用shell建議的序列:N/2和Increment/2。使用希爾增量時希爾排序的最壞情形運行時間爲O(n^2)。
3、冒泡排序
基本思想:首先將第一個記錄的關鍵字和第二個記錄的關鍵字進行比較,若爲逆序,則將兩個記錄交換之,然後比較第二個記錄和第三個記錄的關鍵字。依次類推,直至第n-1個記錄和第n個記錄的關鍵字進行過比較爲止。上述過程稱做第一趟冒泡排序,其結果使得關鍵字最大的記錄被安置到最後一個記錄的位置上。然後進行第二趟冒泡排序,對前n-1個記錄進行同樣操作,其結果是使關鍵字次大的記錄被安置到第n-1個記錄的位置上。一般地,第i趟冒泡排序是從1到n-i+1依次比較相鄰兩個關鍵字,並在“逆序”時交換相鄰記錄,其結果是這n-i+1個記錄中關鍵字最大的記錄被交換到第n-i+1的位置上。判別冒泡排序結束的條件應該是“在一趟排序過程中沒有進行過交換記錄的操作”。
示例代碼1:
- void bubblesort(ElementType A[], int N)
- {
- int i, j;
- ElementType tmp;
- for(i = 0; i < N; i++)
- {
- for(j = 0; j < N-i; j++){
- if(A[j] > A[j+1]){
- tmp = A[j];
- A[j] = A[j+1];
- A[j+1] = tmp;
- }
- }
- }
- }
示例代碼2:
- void bubblesort(ElementType a[], int n)
- {
- int j;
- bool flag;
- ElementType tmp;
- flag = true;
- while(flag){
- flag = false;
- for(j = 1; j < n; j++){
- if(a[j-1] > a[j]){
- tmp = a[j-1];
- a[j-1] = a[j];
- a[j] = tmp;
- flag = true;
- }
- }
- n--;
- }
- }
冒泡排序的時間複雜度爲O(n^2)。效率比較底下,當數據量比較小的時候,可以採用冒泡排序。
4、簡單選擇排序
基本思想:每一趟在n-i+1(i=1,2,…,n-1)個記錄中選取關鍵字最小的記錄作爲有序序列中第i個記錄。直接選擇排序和直接插入排序類似,都將數據分爲有序區和無序區,所不同的是直接插入排序是將無序區的第一個元素直接插入到有序區以形成一個更大的有序區,而直接選擇排序是從無序區選一個最小的元素直接放到有序區的最後。
示例代碼:
- void Selectsort(int a[], int n)
- {
- int i, j, nMinIndex, tmp;
- for(i = 0; i < n; i++){
- nMinIndex = i;
- for(j = i + 1; j < n; j++)
- if(a[j] < a[nMinIndex])
- nMinIndex = j;
- tmp = a[i];
- a[i] = a[nMinIndex];
- a[nMinIndex] = tmp;
- }
- }
簡單選擇排序的時間複雜度是O(n^2)。
5、快速排序
基本思想:快速排序是對冒泡排序的一種改進。它的基本思想是,通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有效。
一趟快速排序的具體做法是:附設兩個指針low和high,它們的初值分別爲low和high,設樞軸記錄的關鍵字爲pivotkey,則首先從high所指位置起向前搜索找到第一個關鍵字小於prvotkey的記錄和樞軸記錄互相交換,然後從low所指位置起向後搜索,找到第一個關鍵字大於privotkey的記錄和樞軸記錄互相交換,重複這兩步直至low=high爲止。
示例代碼1:
- void Swap(ElementType *left, ElementType *right)
- {
- ElementType temp = *left;
- *left = *right;
- *right = temp;
- }
- int Partition(ElementType A[], int low, int high)
- {
- ElementType pivotkey = A[low];
- while(low < high){
- while(low < high && A[high] >= pivotkey)
- high--;
- Swap(&A[low], &A[high]);
- while(low < high && A[low] <= pivotkey)
- low++;
- Swap(&A[low], &A[high]);
- }
- return low;
- }
- void QSort(ElementType A[], int low, int high)
- {
- int pivotloc;
- if(low < high){
- pivotloc = Partition(A, low, high);
- QSort(A, low, pivotloc - 1);
- QSort(A, pivotloc + 1, high);
- }
- }
- void QuickSort(ElementType A[], int low, int high)
- {
- QSort(A, low, high);
- }
快速排序的平均時間爲O(n) = nlogn;它是目前被認爲的最好的一種內部排序方法。
6、歸併排序
基本思想:將兩個或兩個以上的有序表組合成一個新的有序表。2-路歸併排序爲例:假設初始序列含有n個記錄,則可看成是n個有序的子序列,每個子序列的長度爲1,然後兩兩歸併,得到n/2(或n/2+1)個長度爲2或1的有序子序列;再兩兩歸併,……如此重複,直至得到一個長度爲n的有序序列爲止。
示例代碼:
- void Merge(ElementType A[], ElementType TmpArray[], int Lpos, int Rpos, int RightEnd)
- {
- int i, LeftEnd, NumElements, TmpPos;
- LeftEnd = Rpos - 1;
- TmpPos = Lpos;
- NumElements = RightEnd - Lpos + 1;
- /*main loop*/
- while(Lpos <= LeftEnd && Rpos <= RightEnd)
- if(A[Lpos] <= A[Rpos])
- TmpArray[TmpPos++] = A[Lpos++];
- else
- TmpArray[TmpPos++] = A[Rpos++];
- while(Lpos <= LeftEnd) /*Copy rest of first half*/
- TmpArray[TmpPos++] = A[Lpos++];
- while(Rpos <= RightEnd) /*Copy rest of second half*/
- TmpArray[TmpPos++] = A[Rpos++];
- /*Copy TmpArray back*/
- for(i = 0; i < NumElements; i++, RightEnd--)
- A[RightEnd] = TmpArray[RightEnd];
- }
- void MSort(ElementType A[], ElementType TmpArray[], int Left, int Right)
- {
- int Center;
- if(Left < Right){
- Center = (Left + Right) / 2;
- MSort(A, TmpArray, Left, Center);
- MSort(A, TmpArray, Center + 1, Right);
- Merge(A, TmpArray, Left, Center + 1, Right);
- }
- }
- void Mergesort(ElementType A[], int N)
- {
- ElementType *TmpArray;
- TmpArray = (ElementType *)malloc(N*sizeof(ElementType));
- if(TmpArray == NULL){
- fprintf(stderr, "no space for tmp array!\n");
- return;
- }
- MSort(A, TmpArray, 0, N-1);
- free(TmpArray);
- return;
- }
歸併排序的效率比較高,設數列長爲N,將數列分開成小數列一共要logN步,每步都是一個合併有序的數列的過程,時間複雜度記爲O(N),因此時間複雜度是O(N*LogN)。它很難用於主存排序,主要問題在於合併兩個排序的表需要線性附加內存,在整個算法中還要花費將數據拷貝到臨時數組再拷貝回來這樣一些附加的工作,其結果嚴重放慢了排序的速度。
7、堆排序
堆是具有下列性質的完全二叉樹:每個節點的值都大於或等於其左右孩子節點的值,稱爲大頂堆;或者每個節點的值都小於或等於其左右孩子節點的值,稱爲小頂堆。
堆排序就是利用堆進行排序的方法.基本思想是:將待排序的序列構造成一個大頂堆.此時,整個序列的最大值就是堆頂 的根結點.將它移走(其實就是將其與堆數組的末尾元素交換, 此時末尾元素就是最大值),然後將剩餘的n-1個序列重新構造成一個堆,這樣就會得到n個元素的次大值.如此反覆執行,便能得到一個有序序列了。 時間複雜度爲 O(nlogn),好於冒泡,簡單選擇,直接插入的O(n^2)
示例代碼:
- #define LeftChild(i) (2*(i) + 1)
- void Swap(ElementType *pa, ElementType *pb)
- {
- ElementType *pc = pa;
- pa = pb;
- pb = pc;
- }
- void PercDown(ElementType A[], int i, int N)
- {
- int Child;
- ElementType Tmp;
- for(Tmp = A[i]; LeftChild(i) < N; i = Child){
- Child = LeftChild(i);
- if(Child != N-1 && A[Child + 1] > A[Child])
- Child++;
- if(Tmp < A[Child])
- A[i] = A[Child];
- else
- break;
- }
- A[i] = Tmp;
- }
- void Heapsort(ElementType A[], int N)
- {
- int i;
- for(i = N/2; i >= 0; i--) /*BuildHeap*/
- PercDown(A, i, N);
- for(i = N - 1; i > 0; i--){
- Swap(&A[0], &A[i]); /*DeleteMax*/
- PercDown(A, 0, i);
- }
- }
總結:
上面雖然給了7種內部排序的方法,可簡單的對它們大致分爲以下幾類:插入排序(直接插入排序、希爾排序)、快速排序(冒泡排序、快速排序)、選擇排序(簡單選擇排序、堆排序)、歸併排序和基數排序。
各內部排序方法的比較:
1、平均時間性能而言,快速排序最佳,其所需時間最省,但快速排序的最壞情況下的時間性能不如堆排序和歸併排序。而後兩者相比較的結果是,在n較大時,歸併排序所需時間較堆排序省,但它所需的輔助存儲量最多。
2、簡單排序包括除希爾排序之外的所有插入排序,冒泡排序和簡單選擇排序,其中以直接插入排序爲最簡單,當序列中的記錄"基本有序"或n值較小時,它是最佳的排序方法,因此常將它和其他的排序方法,諸如快速排序、歸併排序等結合在一起使用。
3、基數排序的實際複雜度可寫成O(d*n)。它最適用於n值很大而關鍵字較小的序列。若關鍵字也很大,而序列中大多數記錄的"最高位關鍵字"均不同,則亦可先按"最高位關鍵字"不同將序列分成若干"小"的子序列,而後進行直接插入排序。
4、從方法的穩定性來比較,基數排序是穩定的內排方法,所有時間複雜度爲O(n^2)的簡單排序法也是穩定的,然而,快速排序、堆排序和希爾排序等時間性能較好的排序方法都是不穩定的。