一.基本概念
1、穩定排序和非穩定排序
簡單地說就是所有相等的數經過某種排序方法後,仍能保持它們在排序之前的相對次序,就說這種排序方法是穩定的。反之,就是非穩定的。
2、內排序和外排序
排序過程中,所有需要排序的數都在內存,並在內存中調整它們的存儲順序,稱為內排序;
排序過程中,只有部分數被調入內存,並借助內存調整數在外存中的存放順序排序方法稱為外排序。
3、算法的時間復雜度和空間復雜度
時間復雜度,是指執行算法所需要的計算工作量。
空間復雜度,一般是指執行這個算法所需要的內存空間。
二.排序算法實現
-----------------------------------------------------------------
插入排序
有一個已經有序的數據序列,要求在這個已經排好的數據序列中插入一個數,但要求插入後此數據序列仍然有序,這個時候就要用到一種新的排序方法——插入排序法。
插入排序的基本操作就是將一個數據插入到已經排好序的有序數據中,從而得到一個新的、個數加一的有序數據,算法適用於少量數據的排序,時間復雜度為O(n^2)。是穩定的排序方法。
插入算法把要排序的數組分成兩部分:第一部分包含了這個數組的所有元素,但將最後一個元素除外,而第二部分就只包含這一個元素。在第一部分排序後,再把這個最後元素插入到此刻已是有序的第一部分裡的位置
1.直接插入排序穩定)
直接插入排序的過程為:在插入第i個記錄時,R1,R2,..Ri-1已經排好序,將第i個記錄的排序碼Ki依次和R1,R2,..,Ri-1的排序碼逐個進行比較,找到適當的位置。使用直接插入排序,對於具有n個記錄的文件,要進行n-1趟排序。
tips:在C語言中是無法為一個函數傳遞一個數組過去的,因為在C世界裡有一個潛規則:
C語言中,當一維數組作為函數參數的時候,編譯器就是把它解析為一個指向其首元素地址的指針變量同樣作變返回值時,也是不能如願的,仍是以指針形式返回的,當然數組變身為一個指針後,其長度就需要單獨的一個參考來傳遞了。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Dir_Insert(int *x,int N);
int main(int argc,char *argv[])
{
int len=0,i=0,strl,j;
char *result=NULL;
strl=strlen(argv[1]);
int copy[strl];
char *str=argv[1];
char *res1=strtok(str,"[");
char *res2=strtok(res1,"]");
result=strtok(res2,",");
while(result!=NULL)
{
copy[i]=atoi(result);//將argv[1]中的字符串取出轉換為int存入足夠大的數組
result=strtok(NULL,",");
len++;
i++;
}
Dir_Insert(copy,len);
for(j=0;j<len;j++)
{
printf("%d ",copy[j]);
}
printf("\n");
exit(0);
}
void Dir_Insert(int *x,int N)
{
int i,j,tmp;
for(i=1;i<N;i++)
{
tmp=*(x+i);
j=i-1;
while(*(x+j)>tmp && j>=0)
{
*(x+j+1)=*(x+j);
j--;
}
*(x+j+1)=tmp;
}
}
結果為:
2.希爾排序(不穩定)
Shell排序的基本思想是:先取一個小於n的整數d1作為第一個增量把文件的全部記錄分成d1個組。所有距離為d1的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;然後,取得第二個增量d2<d1重復上述的分組和排序,直至所取的增量di=1,即所有記錄放在同一組中進行直接插入排序為止。該方法實質上是一種分組插入方法。
void shell_sort(int *x, int N)
{
int h, j, k, t;
for (h=N/2; h>0; h=h/2)//保證增量為奇數
{
for (j=h; j<N; j++)
{
t = *(x+j);
for (k=j-h; (k>=0 && t<*(x+k)); k-=h)
{
*(x+k+h) = *(x+k);
}
*(x+k+h) = t;
}
}
}
-----------------------------------------------------------------
選擇排序
設數組內存放了n個待排數字,數組下標從1開始,到n結束。
i=1
從數組的第i個元素開始到第n個元素,尋找最小的元素。
將上一步找到的最小元素和第i位元素交換。
如果i=n-1算法結束,否則回到第3步
①.直接選擇排序(不穩定)
直接選擇排序的過程是:首先在所有記錄中選出序碼最小的記錄,把它與第1個記錄交換,然後在其余的記錄內選出排序碼最小的記錄,與第2個記錄交換......依次類推,直到所有記錄排完為止。
無論文件初始狀態如何,在第i趟排序中選出最小關鍵字的記錄,需要做n-i次比較,因此,總的比較次數為n(n-1)/2=O(n^2)。當初始文件為正序時,移動次數為0;文件初態為反序時,每趟排序均要執行交換操作,總的移動次數取最大值3(n-1)。直接選擇排序的平均時間復雜度為O(n^2)。直接選擇排序是不穩定的。
void select_sort(int *x, int n)
{
int i, j, min, t;
for (i=0; i<n-1; i++) /*要選擇的次數:0~n-2共n-1次*/
{
min = i; /*假設當前下標為i的數最小,比較後再調整*/
for (j=i+1; j<n; j++)/*循環找出最小的數的下標是哪個*/
{
if (*(x+j) < *(x+min))
{
min = j; /*如果後面的數比前面的小,則記下它的下標*/
}
}
if (min != i) /*如果min在循環中改變了,就需要交換數據*/
{
t = *(x+i);
*(x+i) = *(x+min);
*(x+min) = t;
}
}
}
②.堆排序(不穩定)
首先新建一個空列表,作用與插入排序中的"有序列表"相同。 找到數列中最大的數字,將其加在"有序列表"的末尾,並將其從原數列中刪除。 重復2號步驟,直至原數列為空。 堆排序的平均時間復雜度為nlogn,效率高因為有堆這種數據結構以及它奇妙的特征,使得"找到數列中最大的數字"這樣的操作只需要O(1)的時間復雜度,維護需要logn的時間復雜度),但是實現相對復雜可以說是這裡7種算法中比較難實現的)。 看起來似乎堆排序與插入排序有些相像,但他們其實是本質不同的算法。至少,他們的時間復雜度差了一個數量級,一個是平方級的,一個是對數級的。
堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。n個關鍵字序列
K1,K2,...,Kn稱為堆,當且僅當該序列滿足(Ki<=K2i且Ki<=K2i+1)或(Ki>=K2i且Ki>=K2i+1),(1<=i<=n/2)。根結點(堆頂)的關鍵字是堆裡所有結點關鍵字中最小者,稱為小根堆;根結點的關鍵字是堆裡所有結點關鍵字中最大者,稱為大根堆。
若將此序列所存儲的向量R[1..n]看作是一棵完全二叉樹的存儲結構,則堆實質上是滿足如下性質的完全二叉樹:樹中任一非葉結點的關鍵字均不大於(或不小於)其左右孩子(若存在)結點的關鍵字。
堆排序的關鍵步驟有兩個:一是如何建立初始堆;二是當堆的根結點與堆的最後一個結點交換後,如何對少了一個結點後的結點序列做調整,使之重新成為堆。堆排序的最壞時間復雜度為O(nlog2n),堆排序的平均性能較接近於最壞性能。由於建初始堆所需的比較 次數較多,所以堆排序不適宜於記錄較少的文件。堆排序是就地排序,輔助空間為O(1),它是不穩定的排序方法。
代碼略...
====================================
三.交換排序
兩兩比較待排序記錄的排序碼,並交換不滿足順序要求的那寫偶對,直到滿足條件為止。交換排序的主要方法有冒泡排序和快速排序.
①.冒泡排序(穩定的)
在要排序的一組數中,對當前還未排好序的范圍內的全部數,自上而下對相鄰的兩個數依次進行比較和調整,讓較大的數往下沉,較小的往上冒。即:每當兩相鄰的數比較後發現它們的排序與排序要求相反時,就將它們互換。
void BubbleSort(int *x,int N)
{
int tmp,i,j;
for (i=N-1;i>0;i--)
{
for (j=0;j<i;j++)
{
if (*(x+j)>*(x+j+1))
{
tmp=*(x+j);
*(x+j)=*(x+j+1);
*(x+j+1)=tmp;
}
}
}
}
②.快速排序:(不穩定的)
快速排序是對冒泡排序的一種本質改進。它的基本思想是通過一趟掃描後,使得排序序列的長度能大幅度地減少。在冒泡排序中,一次掃描只能確保最大數值的數移到正確位置,而待排序序列的長度可能只 減少1。快速排序通過一趟掃描,就能確保某個數以它為基准點吧) 的左邊各數都比它小,右邊各數都比它大。然後又用同樣的方法處理 它左右兩邊的數,直到基准點的左右只有一個元素為止。
顯然快速排序可以用遞歸實現,當然也可以用棧化解遞歸實現。下面的 函數是用遞歸實現的,有興趣的朋友可以改成非遞歸的。
快速排序是不穩定的。最理想情況算法時間復雜度O(nlog2n),最壞O(n2)
void quick_sort(int *x, int low, int high)
{
int i, j, t;
if (low < high) /*要排序的元素起止下標,保證小的放在左邊,大的放在右邊。這裡以下標為low的元素為基准點*/
{
i = low;
j = high;
t = *(x+low); /*暫存基准點的數*/
while (i<j) /*循環掃描*/
{
while (i<j && *(x+j)>t) /*在右邊的只要比基准點大仍放在右邊*/
{
j--; /*前移一個位置*/
}
if (i<j)
{
*(x+i) = *(x+j); /*上面的循環退出:即出現比基准點小的數,替換基准點的數*/
i++; /*後移一個位置,並以此為基准點*/
}
while (i<j && *(x+i)<=t) /*在左邊的只要小於等於基准點仍放在左邊*/
{
i++; /*後移一個位置*/
}
if (i<j)
{
*(x+j) = *(x+i); /*上面的循環退出:即出現比基准點大的數,放到右邊*/
j--; /*前移一個位置*/
}
}
*(x+i) = t; /*一遍掃描完後,放到適當位置*/
quick_sort(x,low,i-1); /*對基准點左邊的數再執行快速排序*/
quick_sort(x,i+1,high); /*對基准點右邊的數再執行快速排序*/
}
}
==================================
四.歸並排序
歸並排序是將兩個或兩個以上的有序子表合並成一個新的有序表。初始時,把含有n個結點的待排序序列看作由n個長度都為1的有序子表組成,將它們依次兩兩歸並得到長度為2的若干有序子表,再對它們兩兩合並。直到得到長度為n的有序表,排序結束。
歸並排序是一種穩定的排序,可用順序存儲結構,也易於在鏈表上實現,對長度為n的文件,需進行log2n趟二路歸並,每趟歸並的時間為O(n),故其時間復雜度無論是在最好情況下還是在最壞情況下均是O(nlog2n)。歸並排序需要一個輔助向量來暫存兩個有序子文件歸並的結果,故其輔助空間復雜度為O(n),顯然它不是就地排序。
代碼略...
===================================
總結
按平均時間將排序分為四類:
1)平方階(O(n2))排序
一般稱為簡單排序,例如直接插入、直接選擇和冒泡排序;
2)線性對數階(O(nlgn))排序
如快速、堆和歸並排序;
3)O(n1+£)階排序
£是介於0和1之間的常數,即0<£<1,如希爾排序;
4)線性階(O(n))排序
如基數排序。
各種排序方法比較
簡單排序中直接插入最好,快速排序最快,當文件為正序時,直接插入和冒泡均最佳。
不同條件下,排序方法的選擇
(1)若n較小(如n≤50),可采用直接插入或直接選擇排序。
(2)若文件初始狀態基本有序(指正序),則應選用直接插人、冒泡或隨機的快速排序為宜;
(3)若n較大,則應采用時間復雜度為O(nlgn)的排序方法:快速排序、堆排序或歸並排序。
快速排序是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分布時,快速排序的平均時間最短;
堆排序所需的輔助空間少於快速排序,並且不會出現快速排序可能出現的最壞情況。這兩種排序都是不穩定的。 若要求排序穩定,則可選用歸並排序。但從單個記錄起進行兩兩歸並的 排序算法並不值得提倡,通常可以將它和直接插入排序結合在一起使用。先利用直接插入排序求得較長的有序子文件,然後再兩兩歸並之。因為直接插入排序是穩定的,所以改進後的歸並排序仍是穩定的。