1、內排序 算法
排序類別 | 排序方法 | 最好時間複雜度 | 平均時間複雜度 | 最壞時間複雜度 | 輔助空間 | 穩定性 | 備註 |
插入類 | 插入 | O(n) | O(n2) | O(n2) | O(1) | 穩定 | 大部分已排序時較好 |
希爾排序 | - | O(ns),1<s<2 | - | O(1) | 不穩定 | s是所選分組 | |
交換類 | 冒泡排序 | O(n) | O(n2) | O(n2) | O(1) | 穩定 | n小時較好 |
快速排序 | O(nlogn) | O(nlogn) | O(n2) | O(logn) | 不穩定 | n大時較好 | |
選擇類 | 選擇 | O(n2) | O(n2) | O(n2) | O(1) | 不穩定 | n小時較好 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不穩定 | n大時較好 | |
歸併排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 穩定 | n大時較好 | |
基數排序 | O(d(n+rd)) | O(d(n+rd)) | O(d(n+rd)) | O(rd) | 穩定 | 見下文 |
1.插入排序(InsertSort)shell
插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。固然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,若是比它大則直接插入在其後面,不然一直往前找直到找到它該插入的位置。若是遇見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。這樣,相等元素的先後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,因此插入排序是穩定的。api
插入排序是對冒泡排序的改進。它比冒泡排序快2倍。通常不用在數據大於1000的場合下使用插入排序,或者重複排序超過200數據項的序列。數組
void insertSort(int a[],int n) //插入排序 { int i,j; int t; for(i=1;i<n;i++) { t=a[i]; //保存當前無序表中的第一個數據 j=i-1; while(j>=0 && a[j]>t) { a[j+1]=a[j]; j--; } a[j+1]=t; //將數據插入有序表中 } }
2.希爾排序(ShellSor)ui
希爾排序是按照不一樣步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,因此插入排序的元素個數不多,速度很快;當元素基本有序了,步長很小,插入排序對於有序的序列效率很高。因此,希爾排序的時間複雜度會比O(n2)好一些。因爲屢次插入排序,咱們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不一樣的插入排序過程當中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,因此shell排序是不穩定的。spa
Shell排序的分組合理性會對算法產生重要的影響。如今多用D.E.Knuth的分組方法。Shell排序比冒泡排序快5倍,比插入排序大體快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢不少。可是它相對比較簡單,它適合於數據量在5000如下而且速度並非特別重要的場合。它對於數據量較小的數列重複排序是很是好的。設計
void shellSort(int a[],int n) //希爾排序 { int i,j,gap; int t; for(gap=n/2;gap>0;gap/=2) for(i=gap;i<n;i++) { t=a[i]; j=i-gap; while(j>=0 &&a[j]>t) { a[j+gap]=a[j]; j-=gap; } a[j+gap]=t; } }
3.冒泡排序(BubbleSort)code
冒泡排序就是把小的元素往前調或者把大的元素日後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。因此,若是兩個元素相等,就不會再把它們倆交換一下的;若是兩個相等的元素沒有相鄰,那麼即便經過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,相同元素的先後順序並無改變,因此冒泡排序是一種穩定排序算法。blog
冒泡排序是最慢的排序算法,在實際運用中它是效率最低的算法。排序
void bubbleSort(int a[],int n) //冒泡排序 { int i,j; int t; for(i=0;i<n;i++) for(j=0;j<n-i-1;j++) if(a[j]>a[j+1]) { t=a[j]; a[j]=a[j+1]; a[j+1]=t; } }
4.快速排序(QuickSort)
快速排序有兩個方向,左邊的i下標一直往右走,當a[i]<= a[center_inde],其中center_index是中樞元素的數組下標,通常取爲數組第0個元素。而右邊的j下標一直往左走,當a[j]> a[center_index]。若是i和j都走不動了,i<= j, 和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]交換的時刻。
快速排序是一個就地排序,分而治之,大規模遞歸的算法。從本質上來講,它是歸併排序的就地版本。快速排序能夠由下面四步組成。
⑴若是很少於1個數據,直接返回。
⑵通常選擇序列最左邊的值做爲支點數據。
⑶將序列分紅2部分,一部分都大於支點數據,另一部分都小於支點數據。
⑷對兩邊利用遞歸排序數列。
快速排序比大部分排序算法都要快。儘管咱們能夠在某些特殊的狀況下寫出比快速排序快的算法,可是就一般狀況而言,沒有比它更快的了。快速排序是遞歸的,對於內存很是有限的機器來講,它不是一個好的選擇。
void quickSort(int a[],int s,int e) //對a[s]至a[e]的元素進行快速排序 { int i=s,j=e; int t; if(s<e) { t=a[s]; while(i!=j) { while(j>i && a[j]>t) j--; //從右向左掃描,找第一個小於t的a[j] if(i<j) //表示找到這樣的a[j] { a[i]=a[j]; i++; } while(i<j && a[i]<=t) i++; //從左向右掃描,找第一個大於t的a[i] if(i<j) //表示找到這樣的a[i] { a[j]=a[i]; j--; } } a[i]=t; //將a[s]放到a[s]至a[e]的恰當位置i處,使得其左邊的元素都不大於它,其右邊的元素都不小於它。 quickSort(a,s,i-1); //對左區間遞歸排序 quickSort(a,i+1,e); //對右區間遞歸排序 } }
5.選擇排序(SelectSort)
選擇排序是給每一個位置選擇當前元素最小的,好比給第一個位置選擇最小的,在剩餘元素裏面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,由於只剩下它一個最大的元素了。那麼,在一趟選擇,若是當前元素比一個元素小,而該小的元素又出如今一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。例如,序列{5,8,5,2,9},第一趟選擇第1個元素5會和2交換,那麼原序列中2個5的相對先後順序就被破壞了,因此選擇排序不是一個穩定的排序算法。
在實際應用中處於和冒泡排序基本相同的地位。它們只是排序算法發展的初級階段,在實際中使用較少。
void selectSort1(int a[],int n) //選擇排序 { int i,j; int t; for(i=0;i<n-1;i++) //數據起始位置,從0到倒數第二個數據 for(j=i+1;j<n;j++) ////在剩下的數據中循環 { if(a[i]>a[j]) // //若是有比它小的,交換二者 { t=a[i]; a[i]=a[j]; a[j]=t; } } } void selectSort2(int a[],int n) //選擇排序的改進,減小了交換的次數 { int i,j,small; int t; for(i=0;i<n-1;i++) //數據起始位置,從0到倒數第二個數據 { small=i; //記錄最小數據的下標 for(j=i+1;j<n;j++) //在剩下的數據中尋找最小數據 { if(a[j]<a[small]) //若是有比它更小的,記錄下標 small=j; } t=a[small]; //將最小數據和未排序的第一個數據交換 a[small]=a[i]; a[i]=t; } }
6.堆排序(HeapSort)
堆的結構是結點i的孩子爲2i和2i+1節點,大頂堆要求父結點大於等於其2個子結點,小頂堆要求父結點小於等於其2個子結點。在一個長爲n的序列,堆排序的過程是從第n/2開始和其子結點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇固然不會破壞穩定性。但當爲n/2-1,n/2-2, ...1這些個父結點選擇元素時,就會破壞穩定性。有可能第n/2個父節點交換把後面一個元素交換過去了,而第n/2-1個父結點把後面一個相同的元素沒有交換,那麼這2個相同的元素之間的穩定性就被破壞了。因此,堆排序不是穩定的排序算法。
堆排序適合於數據量很是大的場合(百萬數據)。堆排序不須要大量的遞歸或者多維的暫存數組。這對於數據量很是巨大的序列是合適的。好比超過數百萬條記錄,因爲快速排序,歸併排序都使用遞歸來設計算法,在數據量很是大的時候,可能會發生堆棧溢出錯誤。堆排序會將全部的數據建成一個堆,最大的數據在堆頂,而後將堆頂數據和序列的最後一個數據交換。接下來再次重建堆,交換數據,依次下去,就能夠排序全部的數據。
void max_heapify(int a[], int start, int end) //調整爲大頂堆 { //父結點和子結點下標 int dad = start, son = start * 2 + 1; while(son <= end) //子結點下標在數組範圍內才能比較 { //先比較左右孩子的大小,選擇大孩子的下標 if(son+1 <= end && a[son+1] > a[son]) son++; if(a[son] > a[dad]) { int t = a[son]; a[son] = a[dad]; a[dad] = t; dad = son; son = dad * 2 + 1; } else break; } } void heapSort(int a[], int n) //堆排序 { int i; //初始化數組爲大頂堆,i=n/2-1表示最後一個父結點的下標 for(i=n/2-1; i>=0; i--) max_heapify(a, i, n-1); for(i=n-1; i>0; i--) //根做爲最大值調整到當前序列的最後 { int t = a[0]; a[0] = a[i]; a[i] = t; max_heapify(a, 0, i-1); } }
7.歸併排序(MergeSort)
歸併排序是把序列遞歸地分紅短序列,遞歸出口是短序列只有1個元素(認爲直接有序)或者2個序列(1次比較和交換),而後把各個有序的段序列合併成一個有序的長序列,不斷合併直到原序列所有排好序。能夠發現,在1個或2個元素時,1個元素不會交換,2個元素若是大小相等也沒有人故意交換,這不會破壞穩定性。那麼,在短的有序序列合併的過程當中,穩定是是否受到破壞?沒有,合併過程當中咱們能夠保證若是兩個當前元素相等時,咱們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。因此,歸併排序也是穩定的排序算法。
歸併排序比堆排序稍微快一點,可是須要比堆排序多一倍的內存空間,由於它須要一個額外的數組。
void mergearray(int a[], int first, int mid, int last, int temp[]) //將有二個有序數列a[first...mid]和a[mid+1...last]合併。 { int i=first, j=mid+1; int m=mid, n=last; int k=0; while (i<=m && j<=n) { if (a[i]<a[j]) temp[k++]=a[i++]; else temp[k++]=a[j++]; } while (i<=m) temp[k++]=a[i++]; while (j<=n) temp[k++]=a[j++]; for (i=0;i<k;i++) a[first+i]=temp[i]; } void mergesort(int a[], int first, int last, int temp[]) { if (first<last) { int mid=(first+last)/2; mergesort(a, first, mid, temp); //左邊有序 mergesort(a, mid+1, last, temp); //右邊有序 mergearray(a, first, mid, last, temp); //再將二個有序數列合併 } } void MergeSort(int a[], int n) { int *p=(int *)malloc(n*sizeof(int)); mergesort(a, 0, n - 1, p); free(p); }
8.基數排序(RadixSort)
基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序,最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,因此其是穩定的排序算法。
基數排序和一般的排序算法並不走一樣的路線。它是一種比較新穎的算法,可是它只能用於整數的排序,若是咱們要把一樣的辦法運用到浮點數上,咱們必須瞭解浮點數的存儲格式,並經過特殊的方式將浮點數映射到整數上,而後再映射回去,這是很是麻煩的事情,所以,它的使用一樣也很少。並且,最重要的是,這樣算法也須要較多的存儲空間。
時間效率:設待排序列爲n個記錄,d個關鍵碼,關鍵碼的取值範圍爲radix,則基數排序的時間複雜度爲O(d(n+radix)),其中,一趟分配時間複雜度爲O(n),一趟收集時間複雜度爲O(radix),共進行d趟分配和收集。
int maxbit(int a[],int n) //求數組元素的最大位數 { int d=1,i=0; //保存最大的位數 int p=10; for(i=0;i<n;i++) { while(a[i]>=p) { p*=10; d++; } } return d; } void radixsort(int a[],int n) //基數排序 { int d = maxbit(a,n); long *tmp=(long *)malloc(n*sizeof(long)); long count[10]; //計數器,統計每位基數的個數 long i,j,k; int radix=1; for(i=1;i<=d;i++) //進行d次排序 { for(j=0;j<10;j++) //每次分配前清空計數器 count[j]=0; for(j=0;j<n;j++) //統計基數出現的次數 { k=(a[j]/radix)%10; count[k]++; } for(j=1;j<10;j++) //將tmp中的位置依次分配給每一個計數器 count[j]=count[j-1]+count[j]; for(j=n-1;j>=0;j--) //根據計數器,將記錄依次收集到tmp中 { k=(a[j]/radix)%10; count[k]--; tmp[count[k]]=a[j]; } for(j=0;j<n;j++) //將臨時數組的內容複製到數組a中 a[j] = tmp[j]; radix = radix*10; } free(tmp); }
2、外排序
當待排序的文件比內存的可以使用容量還大時,文件沒法一次性放到內存中進行排序,須要藉助於外部存儲器(例如硬盤、U盤、光盤),這時就須要用外部排序算法來解決。
外部排序算法由兩個階段構成:
按照內存大小,將大文件分紅若干長度爲 l 的子文件(l 應小於內存的可以使用容量),而後將各個子文件依次讀入內存,使用適當的內部排序算法對其進行排序(排好序的子文件統稱爲「歸併段」或者「順段」),將排好序的歸併段從新寫入外存,爲下一個子文件排序騰出內存空間;
對獲得的順段進行合併,直至獲得整個有序的文件爲止。
例如,有一個含有 10000 個記錄的文件,可是內存的可以使用容量僅爲 1000 個記錄,毫無疑問須要使用外部排序算法,具體分爲兩步:
1. 將整個文件其等分爲 10 個臨時文件(每一個文件中含有 1000 個記錄),而後將這 10 個文件依次進入內存,採起適當的內存排序算法對其中的記錄進行排序,將獲得的有序文件(初始歸併段)移至外存。
2. 對獲得的 10 個初始歸併段進行如圖所示的兩路歸併,直至獲得一個完整的有序文件。
如圖所示有 10 個初始歸併段到一個有序文件,共進行了 4 次歸併,每次都由 m 個歸併段獲得 ⌈m/2⌉ 個歸併段,這種歸併方式被稱爲 2-路平衡歸併。
對於外部排序算法來講,影響總體排序效率的因素主要取決於讀寫外存的次數,即訪問外存的次數越多,算法花費的時間就越多,效率就越低。
對於同一個文件來講,對其進行外部排序時訪問外存的次數同歸並的次數成正比,即歸併操做的次數越多,訪問外存的次數就越多。使用2-路平衡歸併的方式,觸類旁通,還可使用 3-路歸併、4-路歸併甚至是 10-路歸併的方式,下圖爲 5-路歸併的方式:
對於 k-路平衡歸併中 k 值得選擇,增長 k 能夠減小歸併的次數,從而減小外存讀寫的次數,最終達到提升算法效率的目的。除此以外,通常狀況下對於具備 m 個初始歸併段進行 k-路平衡歸併時,歸併的次數爲:s=」logkm」(其中 s 表示歸併次數)。
從公式上能夠判斷出,想要達到減小歸併次數從而提升算法效率的目的,能夠從兩個角度實現:1. 增長 k-路平衡歸併中的 k 值;2. 儘可能減小初始歸併段的數量 m,即增長每一個歸併段的容量。