1. 程式人生 > >幾種常用排序演算法的思路和複雜度對比

幾種常用排序演算法的思路和複雜度對比

1、插入排序——直接插入排序、希爾排序 (1)直接插入排序思路:從第1號元素開始,每個元素依次與前面的元素做比較,小的排前面,這樣當比較到最後一 個元 素完即完成排序。 (2)希爾排序思路:           首先以d1(0<d1<n-1)為步長,把陣列A中n個元素分為d1個組,使下標距離為d1的元素在同一組中;接著在每個組內進行直接插入排序;接著在以d2為步長(d2<d1)在上一次基礎上繼續分組排序,直到dt=1。 d1一般在n/3~n/2之間。   希爾排序是不穩定的。 void ShellSearch( ElemType A[], int n
){
ElemType x; int i ,j, d; for(d=n/2; d>=1;d/=2){   //按不同分量分組
for(i=d;i<n;i++){    //內插
x=A[i];
for(j=i-d; j>=0;j-=d){
if(x<A[j])A[j+d]=A[j];
else break;
}
A[j+d]=x;
}
}
}

2、 選擇排序——直接選擇排序、堆排序 (1) 直接選擇排序: 
選出最小的元素,與第0個元素交換位置,此時第0個元素成為最小的元素;接著,繼續選出最小的與第1個元素交換位置,此時第1號為次小的。。。依次類推,比較到最後一個元素時,排序也完成。

void SelectSort(ElemType A[], int n){
ElemType  x; int i, j,k; for(i=1; i<=n-1; i++){
k=i-1;    //儲存最小元素碼下標
for(j=i; j<=n-1; j++){
if(A[j]<A[k]) k=j;
}
if(k!=i-1){          //把A[k]對調到該排序區間的第一個位置
x=A[i-1];
A[i-1]=A[k];
A[k]=x;
}
}
}

(2)堆排序 對Ri進行篩運算是在其左、右子樹均為堆的基礎上實現的。對Ri進行篩運算的過程可敘述為:首先把Ri的排序碼Si與兩個孩子中排序碼較大者Sj(j=2i+1或j=2i+2)進行比較,若Si>=Sj,則以Si為根的子樹成為堆,篩運算完畢,否則Ri與Rj互換位置,依次類推,直到父結點的排序碼大於等於孩子結點中較大的排序碼或者孩子結點為空為止。這樣以Ri為根的子樹就被調整為一個堆。在對Ri進行的篩運算中,小的被漏下去,大的被留下,所以把構成堆的過程形象地稱為篩運算。 //篩運算 void Sift(ElemType A[], int n, int i){
ElemType x=A[i]; int j; j=2*i+1;   //左孩子 while(j<=n-1){   //當A[i]左孩子非空時執行迴圈
if(j<n-1&&A[j]<A[j+1]) j++;   //右孩子大,把 j 改為右孩子 
if(x<A[ j]){
A[i]=A[j];
i=j;
j=2*i+1;   //繼續往下篩
}
else break;
} A[i]=x;   //被篩點的值放入最終位置
} //堆排序 void HeapSort( ElemType  A[], int n ){
ElemType x; int i; for(i=n/2-1;i>=0; i--) Sift(A, n, i)  //建立初始堆 for(i=1; i<=n-1; i++){
x=A[0];
A[0]=A[n-i];    //將最後一個節點和樹根值互換
A[n-1]=x;
Sift(A, n-i, 0);   //篩A[0]結點,得到n-i個結點的堆
}

3、交換排序——氣泡排序和快速排序
(1) (1)氣泡排序(氣泡排序) 首先將A[n-1]元素的排序碼同A[n-2]元素的排序碼進行比較,若A[n-1]<A[n-2],則交換兩元素位置,使輕者上浮,重者下沉,接著比較 A[n-2]元素的排序碼同A[n-3]元素的排序碼,依次類推,直到A[1]和A[0]比較。這樣A[0]成為最小;然後比較A[n-1]~A[1]進行第二次排序;重複n-1遍後整個氣泡排序完成。 (2)快速排序(劃分排序) 是對氣泡排序的改進,氣泡排序是相鄰元素比較,而快排是從中間往兩邊比較,這樣總的移動次數大大減少。 過程:從待排區間選一基準元素(通常取第一個元素,若不是第一個則與第一個交換),通過兩端往中間比較,大的換到後面,小的換到前面;當所有元素換過之後,把基準元素交換到兩個區間的交界處,這也是該基準元素的最終位置;然後再對兩個區間進行同樣的快速排序過程,當一個區間為空或只有一個元素時,結束快排。 void quickSort(int A[],int s,int t){
int i=s,j=t; int standard=A[s];    //儲存基準值 while(i<=j){
while(i<=j&&A[i]<A[s])i++;
while(i<=j&&A[j]>A[s])j--; if(i<j){
int temp=A[i];
A[i]=A[j];
A[j]=temp;
i++;
j--;
}
         }          if(s!=j){           //把基準值交換到交界處               A[s]=A[j];
              A[j]=standard;
          }          if(s<j-1)quickSort(A,s,j-1);
         if(j+1<t)quickSort(A,j+1,t);
} 把基準元素看作根結點,上述排序過程對應一棵二叉搜尋樹。這是一種不穩定的排序方法。

4、歸併排序 (1)歸併:將兩個或者多個有序表合併成為一個有序表的過程。若將兩個個有序表合併成一個有序表則稱為二路歸併,適合內排序也適合外排序。 二路歸併演算法: void TwoMerge(ElemType A[],  ElemType   R[], int s, int m, int t  ){   //將A陣列兩個相鄰的有序表A[s]~A[m]和A[m+1]~A[t]歸併為R陣列中的一個有序表R[s]~R[t]
int i, j, k; i=s; j=m+1; k=s;  //給每個有序表指標賦初值 while(i<=m&&j<=t){     //兩個表都非空
if(A[i]<A[j]){     
R[k]=A[i]; i++; k++;
}                                                 //把較小的先賦值給R陣列
else{
R[k]=A[j]; j++; k++;
}
} while(i<=m){
R[k]=A[i]; i++; k++;
}                                                         //其中一個為空,把另一個剩下的元素都搬到R陣列 while(j<=t){
R[k]=A[j]; j++; k++;
}
} (2)歸併排序:利用歸併操作把一個無序表排列成一個有序表的過程。利用二路歸併操作則稱為二路歸併排序。 過程:首先,把每個元素看成一個有序表,接著相鄰兩個表歸併,若剩一個表,則直接進入下一輪歸併;然後再兩個表兩兩歸併,如此進行下去直到得到一個長度為n的有序表。 一趟二路歸併排序的演算法描述: void MergePass(ElemType A[],  ElemType  R[], int n; int len ){  //把陣列A[n]中每個長度為len的有序表兩兩歸併到陣列R[n]
int p=0;                                                                     //p為每一對待合併表的第一個元素下標,初始為0 while(p+2*len-1<=n-1){    //兩兩歸併長度為len的有序表
TowMerge(A, R,p,p+len-1,p+2*len-1);
p+=2*len;
} if(p+len-1<=n-1)           //歸併最後兩個長度不等的有序表
TwoMerge(A, R, p,p+len-1, n-1);
else
for(int i=p; i<=n-1 ; i++) 
R[i]=A[i];      //把剩下最後一個有序表複製到R中
} 二路歸併排序演算法 void MergeSort(ElemType A[], int n){
ElemType* R=new  ElemType[n]; int len=1; while(len<n){
MergePass(A, R, n,len);   //A歸併到R
len*=2;
MergePass(R, A, n,len);   //R歸併到A
len*=2;
} delect [] R;
} 時間複雜度O(n*lbn), 空間複雜度O(n)。二路歸併排序是穩定的。
5、各種內排序方法的比較 (1) 時間複雜度:直接插入、直接選擇和冒泡這3中簡單排序方法屬於第一類,其時間複雜度為O(n^2);快速、歸併、堆排序 這3種屬於第二類,其時間複雜度為O(lbn);希爾排序介於兩者中間。最好情況下,直接插入和冒泡最好;平均情況下,快排最好;最壞情況下,歸併和堆排序最好。 (2)空間複雜度:歸併O(n);快排O(lbn);其他O(1) (3) 穩定性:穩定——直接插入、冒泡和歸併;不穩定——直接選擇、希爾、快排、堆排序 (4)資料較少時用簡單排序合適,資料多用改進演算法合適。