十種常見排序算法通常分爲如下幾種:
(1)非線性時間比較類排序:交換類排序(快速排序和冒泡排序)、插入類排序(簡單插入排序和希爾排序)、選擇類排序(簡單選擇排序和堆排序)、歸併排序(二路歸併排序和多路歸併排序);node
(2)線性時間非比較類排序:計數排序、基數排序和桶排序。算法
總結:
(1)在比較類排序中,歸併排序號稱最快,其次是快速排序和堆排序,二者不相伯仲,可是有一點須要注意,數據初始排序狀態對堆排序不會產生太大的影響,而快速排序卻偏偏相反。shell
(2)線性時間非比較類排序通常要優於非線性時間比較類排序,但前者對待排序元素的要求較爲嚴格,好比計數排序要求待排序數的最大值不能太大,桶排序要求元素按照hash分桶後桶內元素的數量要均勻。線性時間非比較類排序的典型特色是以空間換時間。數組
注:本博文的示例代碼均已遞增排序爲目的。ide
交換排序的基本方法是:兩兩比較待排序記錄的排序碼,交換不知足順序要求的偶對,直到所有知足位置。常見的冒泡排序和快速排序就屬於交換類排序。函數
算法思想:
從數組中第一個數開始,依次遍歷數組中的每個數,經過相鄰比較交換,每一輪循環下來找出剩餘未排序數的中的最大數並」冒泡」至數列的頂端。性能
算法步驟:
(1)從數組中第一個數開始,依次與下一個數比較並次交換比本身小的數,直到最後一個數。若是發生交換,則繼續下面的步驟,若是未發生交換,則數組有序,排序結束,此時時間複雜度爲O(n);
(2)每一輪」冒泡」結束後,最大的數將出如今亂序數列的最後一位。重複步驟(1)。ui
穩定性:穩定排序。spa
時間複雜度: O(n)至O(n2),平均時間複雜度爲O(n2)。.net
最好的狀況:若是待排序數據序列爲正序,則一趟冒泡就可完成排序,排序碼的比較次數爲n-1次,且沒有移動,時間複雜度爲O(n)。
最壞的狀況:若是待排序數據序列爲逆序,則冒泡排序須要n-1次趟起泡,每趟進行n-i次排序碼的比較和移動,即比較和移動次數均達到最大值:
比較次數:Cmax=∑i=1n−1(n−i)=n(n−1)/2=O(n2)
移動次數等於比較次數,所以最壞時間複雜度爲O(n2)。
示例代碼:
void bubbleSort(int array[],int len){ //循環的次數爲數組長度減一,剩下的一個數不須要排序 for(int i=0;i<len-1;++i){ bool noswap=true; //循環次數爲待排序數第一位數冒泡至最高位的比較次數 for(int j=0;j<len-i-1;++j){ if(array[j]>array[j+1]){ array[j]=array[j]+array[j+1]; array[j+1]=array[j]-array[j+1]; array[j]=array[j]-array[j+1]; //交換或者使用以下方式 //a=a^b; //b=b^a; //a=a^b; noswap=false; } } if(noswap) break; } }
冒泡排序是在相鄰的兩個記錄進行比較和交換,每次交換隻能上移或下移一個位置,致使總的比較與移動次數較多。快速排序又稱分區交換排序,是對冒泡排序的改進,快速排序採用的思想是分治思想。。
算法原理:
(1)從待排序的n個記錄中任意選取一個記錄(一般選取第一個記錄)爲分區標準;
(2)把全部小於該排序列的記錄移動到左邊,把全部大於該排序碼的記錄移動到右邊,中間放所選記錄,稱之爲第一趟排序;
(3)而後對先後兩個子序列分別重複上述過程,直到全部記錄都排好序。
穩定性:不穩定排序。
時間複雜度: O(nlog2n)至O(n2),平均時間複雜度爲O(nlgn)。
最好的狀況:是每趟排序結束後,每次劃分使兩個子文件的長度大體相等,時間複雜度爲O(nlog2n)。
最壞的狀況:是待排序記錄已經排好序,第一趟通過n-1次比較後第一個記錄保持位置不變,並獲得一個n-1個元素的子記錄;第二趟通過n-2次比較,將第二個記錄定位在原來的位置上,並獲得一個包括n-2個記錄的子文件,依次類推,這樣總的比較次數是:
Cmax=∑i=1n−1(n−i)=n(n−1)/2=O(n2)
示例代碼:
//a:待排序數組,low:最低位的下標,high:最高位的下標 void quickSort(int a[],int low, int high) { if(low>=high) { return; } int left=low; int right=high; int key=a[left]; /*用數組的第一個記錄做爲分區元素*/ while(left!=right){ while(left<right&&a[right]>=key) /*從右向左掃描,找第一個碼值小於key的記錄,並交換到key*/ --right; a[left]=a[right]; while(left<right&&a[left]<=key) ++left; a[right]=a[left]; /*從左向右掃描,找第一個碼值大於key的記錄,並交換到右邊*/ } a[left]=key; /*分區元素放到正確位置*/ quickSort(a,low,left-1); quickSort(a,left+1,high); }
插入排序的基本方法是:每步將一個待排序的記錄,按其排序碼大小,插到前面已經排序的文件中的適當位置,直到所有插入完爲止。
原理:從待排序的n個記錄中的第二個記錄開始,依次與前面的記錄比較並尋找插入的位置,每次外循環結束後,將當前的數插入到合適的位置。
穩定性:穩定排序。
時間複雜度: O(n)至O(n2),平均時間複雜度是O(n2)。
最好狀況:當待排序記錄已經有序,這時須要比較的次數是Cmin=n−1=O(n)。
最壞狀況:若是待排序記錄爲逆序,則最多的比較次數爲Cmax=∑i=1n−1(i)=n(n−1)2=O(n2)。
示例代碼:
//A:輸入數組,len:數組長度 void insertSort(int A[],int len) { int temp; for(int i=1;i<len;i++) { int j=i-1; temp=A[i]; //查找到要插入的位置 while(j>=0&&A[j]>temp) { A[j+1]=A[j]; j--; } if(j!=i-1) A[j+1]=temp; } }
Shell 排序又稱縮小增量排序, 由D. L. Shell在1959年提出,是對直接插入排序的改進。
原理: Shell排序法是對相鄰指定距離(稱爲增量)的元素進行比較,並不斷把增量縮小至1,完成排序。
Shell排序開始時增量較大,分組較多,每組的記錄數目較少,故在各組內採用直接插入排序較快,後來增量di逐漸縮小,分組數減小,各組的記錄數增多,但因爲已經按di−1分組排序,文件叫接近於有序狀態,因此新的一趟排序過程較快。所以Shell排序在效率上比直接插入排序有較大的改進。
在直接插入排序的基礎上,將直接插入排序中的1所有改變成增量d便可,由於Shell排序最後一輪的增量d就爲1。
穩定性:不穩定排序。
時間複雜度:O(n1.3)到O(n2)。Shell排序算法的時間複雜度分析比較複雜,實際所需的時間取決於各次排序時增量的個數和增量的取值。研究證實,若增量的取值比較合理,Shell排序算法的時間複雜度約爲O(n1.3)。
對於增量的選擇,Shell 最初建議增量選擇爲n/2,而且對增量取半直到 1;D. Knuth教授建議di+1=⌊di−13⌋序列。
//A:輸入數組,len:數組長度,d:初始增量(分組數) void shellSort(int A[],int len, int d) { for(int inc=d;inc>0;inc/=2){ //循環的次數爲增量縮小至1的次數 for(int i=inc;i<len;++i){ //循環的次數爲第一個分組的第二個元素到數組的結束 int j=i-inc; int temp=A[i]; while(j>=0&&A[j]>temp) { A[j+inc]=A[j]; j=j-inc; } if((j+inc)!=i)//防止自我插入 A[j+inc]=temp;//插入記錄 } } }
注意:從代碼中能夠看出,增量每次變化取前一次增量的通常,當增量d等於1時,shell排序就退化成了直接插入排序了。
選擇類排序的基本方法是:每步從待排序記錄中選出排序碼最小的記錄,順序放在已排序的記錄序列的後面,知道所有排完。
原理:從全部記錄中選出最小的一個數據元素與第一個位置的記錄交換;而後在剩下的記錄當中再找最小的與第二個位置的記錄交換,循環到只剩下最後一個數據元素爲止。
穩定性:不穩定排序。
時間複雜度: 最壞、最好和平均複雜度均爲O(n2),所以,簡單選擇排序也是常見排序算法中性能最差的排序算法。簡單選擇排序的比較次數與文件的初始狀態沒有關係,在第i趟排序中選出最小排序碼的記錄,須要作n-i次比較,所以總的比較次數是:∑i=1n−1(n−i)=n(n−1)/2=O(n2)。
示例代碼:
void selectSort(int A[],int len) { int i,j,k; for(i=0;i<len;i++){ k=i; for(j=i+1;j<len;j++){ if(A[j]<A[k]) k=j; } if(i!=k){ A[i]=A[i]+A[k]; A[k]=A[i]-A[k]; A[i]=A[i]-A[k]; } } }
直接選擇排序中,第一次選擇通過了n-1次比較,只是從排序碼序列中選出了一個最小的排序碼,而沒有保存其餘中間比較結果。因此後一趟排序時又要重複許多比較操做,下降了效率。J. Willioms和Floyd在1964年提出了堆排序方法,避免這一缺點。
堆的性質:
(1)性質:徹底二叉樹或者是近似徹底二叉樹;
(2)分類:大頂堆:父節點不小於子節點鍵值,小頂堆:父節點不大於子節點鍵值;圖展現一個最小堆:
(3)左右孩子:沒有大小的順序。
(4)堆的存儲
通常都用數組來存儲堆,i結點的父結點下標就爲(i–1)/2。它的左右子結點下標分別爲 2∗i+1 和 2∗i+2。如第0個結點左右子結點下標分別爲1和2。
(5)堆的操做
創建:
以最小堆爲例,若是以數組存儲元素時,一個數組具備對應的樹表示形式,但樹並不知足堆的條件,須要從新排列元素,能夠創建「堆化」的樹。
插入:
將一個新元素插入到表尾,即數組末尾時,若是新構成的二叉樹不知足堆的性質,須要從新排列元素,下圖演示了插入15時,堆的調整。
刪除:
堆排序中,刪除一個元素老是發生在堆頂,由於堆頂的元素是最小的(小頂堆中)。表中最後一個元素用來填補空缺位置,結果樹被更新以知足堆條件。
穩定性:不穩定排序。
插入代碼實現:
每次插入都是將新數據放在數組最後。能夠發現從這個新數據的父結點到根結點必然爲一個有序的數列,如今的任務是將這個新數據插入到這個有序數據中,這就相似於直接插入排序中將一個數據併入到有序區間中,這是節點「上浮」調整。不難寫出插入一個新數據時堆的調整代碼:
//新加入i結點,其父結點爲(i-1)/2 //參數:a:數組,i:新插入元素在數組中的下標 void 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; }
所以,插入數據到最小堆時:
//在最小堆中加入新的數據data //a:數組,index:插入的下標, void minHeapAddNumber(int a[], int index, int data) { a[index] = data; minHeapFixUp(a, index); }
刪除代碼實現:
按定義,堆中每次都只能刪除第0個數據。爲了便於重建堆,實際的操做是將數組最後一個數據與根結點,而後再從根結點開始進行一次從上向下的調整。
調整時先在左右兒子結點中找最小的,若是父結點不大於這個最小的子結點說明不須要調整了,反之將最小的子節點換到父結點的位置。此時父節點實際上並不須要換到最小子節點的位置,由於這不是父節點的最終位置。但邏輯上父節點替換了最小的子節點,而後再考慮父節點對後面的結點的影響。至關於從根結點將一個數據的「下沉」過程。下面給出代碼:
//a爲數組,從index節點開始調整,len爲節點總數 從0開始計算index節點的子節點爲 2*index+1, 2*index+2,len/2-1爲最後一個非葉子節點 void minHeapFixDown(int a[],int len,int index){ if(index>(len/2-1))//index爲葉子節點不用調整 return; int tmp=a[index]; int lastIndex=index; while(index<=(len/2-1)){ //當下沉到葉子節點時,就不用調整了 if(a[2*index+1]<tmp) //若是左子節點大於該節點 lastIndex = 2*index+1; //若是存在右子節點且大於左子節點和該節點 if(2*index+2<len && a[2*index+2]<a[2*index+1]&& a[2*index+2]<tmp) lastIndex = 2*index+2; if(lastIndex!=index){ //若是左右子節點有一個小於該節點則設置該節點的下沉位置 a[index]=a[lastIndex]; index=lastIndex; }else break; //不然該節點不用下沉調整 } a[lastIndex]=tmp;//將該節點放到最後的位置 }
根據思想,能夠有不一樣版本的代碼實現,以上是和孫凜同窗一塊兒討論出的一個版本,在這裏感謝他的參與,讀者可另行給出。我的體會,這裏建議你們根據對堆調整的過程的理解,寫出本身的代碼,切勿看示例代碼去理解算法,而是理解算法思想寫出代碼,不然很快就會忘記。
建堆:
有了堆的插入和刪除後,再考慮下如何對一個數據進行堆化操做。要一個一個的從數組中取出數據來創建堆吧,不用!先看一個數組,以下圖:
很明顯,對葉子結點來講,能夠認爲它已是一個合法的堆了即20,60, 65, 4, 49都分別是一個合法的堆。只要從A[4]=50開始向下調整就能夠了。而後再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分別做一次向下調整操做就能夠了。下圖展現了這些步驟:
寫出堆化數組的代碼:
//創建最小堆 //a:數組,n:數組長度 void makeMinHeap(int a[], int n) { for (int i = n/2-1; i >= 0; i--) minHeapFixDown(a, i, n); }
(6)堆排序的實現
因爲堆也是用數組來存儲的,故對數組進行堆化後,第一次將A[0]與A[n - 1]交換,再對A[0…n-2]從新恢復堆。第二次將A[0]與A[n – 2]交換,再對A[0…n - 3]從新恢復堆,重複這樣的操做直到A[0]與A[1]交換。因爲每次都是將最小的數據併入到後面的有序區間,故操做完成後整個數組就有序了。有點相似於直接選擇排序。
所以,完成堆排序並無用到前面說明的插入操做,只用到了建堆和節點向下調整的操做,堆排序的操做以下:
//array:待排序數組,len:數組長度 void heapSort(int array[],int len){ //建堆 makeMinHeap(array, len); //根節點和最後一個葉子節點交換,並進行堆調整,交換的次數爲len-1次 for(int i=0;i<len-1;++i){ //根節點和最後一個葉子節點交換 array[0] += array[len-i-1]; array[len-i-1] = array[0]-array[len-i-1]; array[0] = array[0]-array[len-i-1]; //堆調整 minHeapFixDown(array, 0, len-i-1); } }
(7)堆排序的性能分析
因爲每次從新恢復堆的時間複雜度爲O(logN),共N - 1次堆調整操做,再加上前面創建堆時N / 2次向下調整,每次調整時間複雜度也爲O(logN)。兩次次操做時間相加仍是O(N * logN)。故堆排序的時間複雜度爲O(N * logN)。
最壞狀況:若是待排序數組是有序的,仍然須要O(N * logN)複雜度的比較操做,只是少了移動的操做;
最好狀況:若是待排序數組是逆序的,不只須要O(N * logN)複雜度的比較操做,並且須要O(N * logN)複雜度的交換操做。總的時間複雜度仍是O(N * logN)。
所以,堆排序和快速排序在效率上是差很少的,可是堆排序通常優於快速排序的重要一點是,數據的初始分佈狀況對堆排序的效率沒有大的影響。
算法思想:
歸併排序屬於比較類非線性時間排序,號稱比較類排序中性能最佳者,在數據中應用中較廣。
歸併排序是分治法(Divide and Conquer)的一個典型的應用。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。
穩定性:穩定排序算法;
時間複雜度: 最壞,最好和平均時間複雜度都是Θ(nlgn)。
具體的實現見本人的另外一篇blog:二路歸併排序簡介及其並行化。
計數排序是一個非基於比較的排序算法,該算法於1954年由 Harold H. Seward 提出,它的優點在於在對於較小範圍內的整數排序。它的複雜度爲Ο(n+k)(其中k是待排序數的範圍),快於任何比較排序算法,缺點就是很是消耗空間。很明顯,若是並且當O(k)>O(n*log(n))的時候其效率反而不如基於比較的排序,好比堆排序和歸併排序和快速排序。
算法原理:
基本思想是對於給定的輸入序列中的每個元素x,肯定該序列中值小於x的元素的個數。一旦有了這個信息,就能夠將x直接存放到最終的輸出序列的正確位置上。例如,若是輸入序列中只有17個元素的值小於x的值,則x能夠直接存放在輸出序列的第18個位置上。固然,若是有多個元素具備相同的值時,咱們不能將這些元素放在輸出序列的同一個位置上,在代碼中做適當的修改便可。
算法步驟:
(1)找出待排序的數組中最大的元素;
(2)統計數組中每一個值爲i的元素出現的次數,存入數組C的第i項;
(3)對全部的計數累加(從C中的第一個元素開始,每一項和前一項相加);
(4)反向填充目標數組:將每一個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。
時間複雜度:Ο(n+k)。
空間複雜度:Ο(k)。
要求:待排序數中最大數值不能太大。
穩定性:穩定。
代碼示例:
#define MAXNUM 20 //待排序數的最大個數 #define MAX 100 //待排序數的最大值 int sorted_arr[MAXNUM]={0}; //計算排序 //arr:待排序數組,sorted_arr:排好序的數組,n:待排序數組長度 void countSort(int *arr, int *sorted_arr, int n) { int i; int *count_arr = (int *)malloc(sizeof(int) * (MAX+1)); //初始化計數數組 memset(count_arr,0,sizeof(int) * (MAX+1)); //統計i的次數 for(i = 0;i<n;i++) count_arr[arr[i]]++; //對全部的計數累加,做用是統計arr數組值和小於小於arr數組值出現的個數 for(i = 1; i<=MAX; i++) count_arr[i] += count_arr[i-1]; //逆向遍歷源數組(保證穩定性),根據計數數組中對應的值填充到新的數組中 for(i = n-1; i>=0; i--) { //count_arr[arr[i]]表示arr數組中包括arr[i]和小於arr[i]的總數 sorted_arr[count_arr[arr[i]]-1] = arr[i]; //若是arr數組中有相同的數,arr[i]的下標減一 count_arr[arr[i]]--; } free(count_arr); }
注意:計數排序是典型的以空間換時間的排序算法,對待排序的數據有嚴格的要求,好比待排序的數值中包含負數,最大值都有限制,請謹慎使用。
基數排序屬於「分配式排序」(distribution sort),是非比較類線性時間排序的一種,又稱「桶子法」(bucket sort)。顧名思義,它是透過鍵值的部分信息,將要排序的元素分配至某些「桶」中,藉以達到排序的做用。
具體描述即代碼示例見本人另外一篇blog:基數排序簡介及其並行化。
桶排序也是分配排序的一種,但其是基於比較排序的,這也是與基數排序最大的區別所在。
思想:桶排序算法想法相似於散列表。首先要假設待排序的元素輸入符合某種均勻分佈,例如數據均勻分佈在[ 0,1)區間上,則可將此區間劃分爲10個小區間,稱爲桶,對散佈到同一個桶中的元素再排序。
要求:待排序數長度一致。
排序過程:
(1)設置一個定量的數組看成空桶子;
(2)尋訪序列,而且把記錄一個一個放到對應的桶子去;
(3)對每一個不是空的桶子進行排序。
(4)從不是空的桶子裏把項目再放回原來的序列中。
例如待排序列K= {4九、 38 、 3五、 97 、 7六、 73 、 2七、 49 }。這些數據所有在1—100之間。所以咱們定製10個桶,而後肯定映射函數f(k)=k/10。則第一個關鍵字49將定位到第4個桶中(49/10=4)。依次將全部關鍵字所有堆入桶中,並在每一個非空的桶中進行快速排序。
時間複雜度:
對N個關鍵字進行桶排序的時間複雜度分爲兩個部分:
(1) 循環計算每一個關鍵字的桶映射函數,這個時間複雜度是O(N)。
(2) 利用先進的比較排序算法對每一個桶內的全部數據進行排序,對於N個待排數據,M個桶,平均每一個桶[N/M]個數據,則桶內排序的時間複雜度爲 ∑i=1MO(Ni∗logNi)=O(N∗logNM) 。其中Ni 爲第i個桶的數據量。
所以,平均時間複雜度爲線性的O(N+C),C爲桶內排序所花費的時間。當每一個桶只有一個數,則最好的時間複雜度爲:O(N)。
示例代碼:
typedef struct node { int keyNum;//桶中數的數量 int key; //存儲的元素 struct node * next; }KeyNode; //keys待排序數組,size數組長度,bucket_size桶的數量 void inc_sort(int keys[],int size,int bucket_size) { KeyNode* k=(KeyNode *)malloc(sizeof(KeyNode)); //用於控制打印 int i,j,b; KeyNode **bucket_table=(KeyNode **)malloc(bucket_size*sizeof(KeyNode *)); for(i=0;i<bucket_size;i++) { bucket_table[i]=(KeyNode *)malloc(sizeof(KeyNode)); bucket_table[i]->keyNum=0;//記錄當前桶中是否有數據 bucket_table[i]->key=0; //記錄當前桶中的數據 bucket_table[i]->next=NULL; } for(j=0;j<size;j++) { int index; KeyNode *p; KeyNode *node=(KeyNode *)malloc(sizeof(KeyNode)); node->key=keys[j]; node->next=NULL; index=keys[j]/10; //映射函數計算桶號 p=bucket_table[index]; //初始化P成爲桶中數據鏈表的頭指針 if(p->keyNum==0)//該桶中尚未數據 { bucket_table[index]->next=node; (bucket_table[index]->keyNum)++; //桶的頭結點記錄桶內元素各數,此處加一 } else//該桶中已有數據 { //鏈表結構的插入排序 while(p->next!=NULL&&p->next->key<=node->key) p=p->next; node->next=p->next; p->next=node; (bucket_table[index]->keyNum)++; } } //打印結果 for(b=0;b<bucket_size;b++) //判斷條件是跳過桶的頭結點,桶的下個節點爲元素節點不爲空 for(k=bucket_table[b];k->next!=NULL;k=k->next) { printf("%d ",k->next->key); } }