html
若是一種排序算法不會改變關鍵碼值相同的記錄的相對順序,則稱爲穩定的(stable)算法
不穩定的算法在某種條件下能夠變爲穩定的算法,而穩定的算法在某種條件下也能夠變爲不穩定的算法。例如,對於冒泡排序算法,本來是穩定的排序算法,若是將記錄交換的條件改爲a[j].key>=a[j+1].key,則兩個相等的記錄就會交換位置。再如,快速排序本來是不穩定的排序方法,但若待排序記錄中只有一組具備相同關鍵碼的記錄,而選擇的軸值剛好是這組相同關鍵碼中的一個,此時的快速排序就是穩定的。shell
1.實際應用時:當容許關鍵碼值重複的時候,具備相同關鍵碼值的記錄之間自己就有某種內在的順序,有些應用要求在排序的時候不改變具備相同關鍵碼值的初始順序,此時,算法穩定性就頗有意義。例如要排序的內容是一組本來按照價格高低排序的對象,現在須要按照銷量高低排序,使用穩定性算法,可使得想同銷量的對象依舊保持着價格高低的排序展示,只有銷量不一樣的纔會從新排序。(固然,若是需求不須要保持初始的排序意義,那麼使用穩定性算法將毫無心義)。api
2.算法設計時:若是排序算法是穩定的,那麼從一個鍵上排序,而後再從另外一個鍵上排序,第一個鍵排序的結果能夠爲第二個鍵排序所利用。基數排序就是這樣,先按低位排序,逐次按高位排序,那麼,低位相同的數據元素其前後位置順序即便在高位也相同時是不會改變的。數組
3.算法優化時:在實際應用中,複雜類型記錄的移動可能會成爲影響程序整個運行時間的重要因素。若是排序算法穩定,對基於比較的排序算法而言,元素交換的次數可能相對會少一些,減小花銷。數據結構
堆排序、快速排序、希爾排序、直接選擇排序是不穩定的排序算法函數
基數排序、冒泡排序、直接插入排序、折半插入排序、歸併排序是穩定的排序算法。性能
直接插入排序,又叫簡單插入排序,逐個處理待排序的記錄,每條新記錄與前面已排序的子序列進行比較,將它插入到子序列的正確位置。學習
插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。若是遇見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。因此,相等元素的先後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,因此插入排序是穩定的。優化
1 template<typename E> 2 void inssort(E A[],int n){ //Insertion Sort 3 for(int i=1;i<n;i++){ //insert i'th record 4 for(int j=i;(j>0)&&(A[j]<A[j-1]);j--){ 5 swap(A,j,j-1); 6 } 7 } 8 } 9 10 template<typename E> 11 void swap(E A[],int i,int j){ 12 E temp=A[i]; 13 A[i]=A[j]; 14 A[j]=temp; 15 }
最好狀況:元素已經遞增有序,每趟只需與前面的最後一個對象比較1次,總的比較次數爲n-1。
最壞狀況:元素遞減有序,第i趟時需比較而且移動前面i個對象。則總的比較次數KCN和移動次數RMN分別爲
平均狀況:對於第i條記錄,數據的前i-1條記錄有一半的關鍵碼比第i條的關鍵碼大,由於平均狀況就是最差狀況的一半,關鍵碼比較次數和對象移動次數約爲(n^2)/4。所以,直接插入排序的時間複雜度爲O(n2)。
冒泡排序就是把小的元素往前調或者把大的元素日後調。(以從小到大爲例)每一輪都是比較相鄰兩個關鍵碼(第1輪1和2,第2輪2和3......),若是遇到低序號的關鍵碼值比高序號的大,則進行兩者交換,不然不進行操做。一旦遇到最小關鍵碼值,這個過程將使它像個「氣泡」被推到數組頂部。所以每一輪都會固定一個當前最小的值。因此,若是兩個元素相等,我想你是不會再無聊地把他們倆交換一下的;若是兩個相等的元素沒有相鄰,那麼即便經過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,因此相同元素的先後順序並無改變,因此冒泡排序是一種穩定排序算法
1 template<typename E> 2 void bubsort(E A[],int n){ 3 for(int i=0;i<n-1;i++){ 4 for(int j=n-1;j>i;j--){ 5 if(A[j]<A[j-1]) 6 swap(A,j,j-1); 7 } 8 } 9 }
冒泡排序的最佳、平均、最差狀況的運行時間幾乎是相同的,比較是不可避免的,交換次數與插入排序的交換次數相同。
選擇排序在第i輪「選擇」第i小的記錄,並把該記錄與數組中的第i個位置上的元素進行交換。其獨特之處在於交換操做的次數不多。選擇排序是不穩定的,舉個栗子,序列5 8 5 2 9, 咱們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對先後順序就被破壞了。
1 template<typename E> 2 void selsort(E A[],int n){ 3 for(int i=0;i<n-1;i++){ 4 int lowindex=i; 5 for(int j=n-1;j>i;j--){ 6 if(A[lowindex]>A[j]) 7 lowindex=j;} 8 swap(A,lowindex,i); 9 } 10 }
選擇排序的比較次數是固定的,總的KCN次數是 n(n-1)/2;
最佳狀況下:RMN=0; 最差狀況:每一趟都要移動,總的RMN=3(n-1)
直接插入排序、冒泡排序和選擇排序三種方法在平均及最差狀況下的時間代價都是O(N2),關鍵的瓶頸在於只比較相鄰元素,所以比較和移動只能一步步的進行(選擇排序除外),交換相鄰記錄稱爲一次交換(exchange),所以,有時這些排序稱爲交換排序(exchange sort)。
希爾排序也叫作縮小增量排序(diminishing increment sort)。shell排序利用了插入排序的最佳時間代價特性,將待排序序列變成基本有序的,而後再利用插入排序來完成最後的排序工做。(基本思想是:對待排記錄序列先做「宏觀」調整,再做「微觀」調整。)
具體的算法步驟是:在執行每一次循環時,將記錄序列分紅若干子序列,並使得子序列中的元素在整個數組中的間距相同(間距稱之爲增量),而後分別對每一個子序列進行插入排序。排序結束以後,減少增量,進行新一輪的排序,直至增量爲1,即常規的插入排序。
顯然是不穩定的。(因爲屢次插入排序,咱們知道一次插入排序是穩定的,不會改變相同元 素的相對順序,但在不一樣的插入排序過程當中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,因此shell排序是不穩定的。)
1 template<typename E> 2 void shellinsert(E A[],int n,int dk){//每一趟插入排序 3 for(int i=incr;i<n;i+=incr){ 4 for(int j=i;(j>=incr)&&(A[j]<A[j-incr]);j-=incr) 5 swap(A,j,j-incr); 6 } 7 } 8 template<typename E> 9 void shellsort(E A[],int n){ 10 for(int i=n/2;i>2;i/=2){ //每個增量(每一輪) 11 for(int j=0;j<i;j++) //每個子序列 12 shellinsert(&A[j],n-j;i); //對每個子序列進行排序,去掉無關的幾個元素 13 } 14 shellinsert(A,n;1); //最後確保全部有序(此時已經基本有序) 15 } 16 17 // Gap的取法有多種。 18 // Shell:gap = n/2,gap = gap/2。 19 // Knuth: gap = n/3+1,gap = gap/3 +1。 20 // 最後一個增量值必須爲1。
對特定的待排序序列,能夠準確地估算關鍵碼的比較次數和對象移動次數。但要弄清關鍵碼比較次數和對象移動次數與增量選擇之間的依賴關係,並給出完整的數學分析,尚未人可以作到。
平均效率是O(nlogn)。其中分組的合理性會對算法產生重要的影響。如今多用D.E.Knuth的分組方法。
Knuth利用大量的實驗統計資料得出,當 n 很大時,關鍵碼平均比較次數和對象平均移動次數大約在 n1.25 到 1.6n1.25 的範圍內。
shell排序說明有時能夠利用一個算法的特殊性能,儘管在通常狀況下該算法可能會慢的讓人難以忍受
首先讓數組中的每個數單獨成爲長度爲1的區間,而後兩兩一組有序合併,獲得長度爲2的有序區間,依次進行,直到合成整個區間。
能夠發現,在1個或2個元素時,1個元素不會交換,2個元素若是大小相等也沒有人故意交換,這不會破壞穩定性。那麼,在短的有序序列合併的過程當中,咱們能夠保證若是兩個當前元素相等時,咱們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。因此,歸併排序也是穩定的排序算法。
1 //歸併排序的標準實現 2 template <class Elem> 3 void mergesort(Elem A[], Elem temp[], 4 int left, int right) { 5 int mid = (left+right)/2; //找到中間界,遞歸處理左右兩個部分 6 if (left == right) return; //將數組分爲間距爲1的數組 7 mergesort<Elem>(A, temp, left, mid); //遞歸處理左邊的部分 8 mergesort<Elem>(A, temp, mid+1, right); 9 for (int i=left; i<=right; i++) // 使用輔助數組存儲待處理的組 10 temp[i] = A[i]; 11 int i1 = left; int i2 = mid + 1; 12 for (int curr=left; curr<=right; curr++) { 13 if (i1 == mid+1) // 左邊界檢查 14 A[curr] = temp[i2++]; 15 else if (i2 > right) //右邊界檢查 16 A[curr] = temp[i1++]; 17 else if (temp[i1]<temp[i2])) 18 A[curr] = temp[i1++]; 19 else A[curr] = temp[i2++]; 20 }} 21 22 //歸併排序的優化實現 23 template <class Elem,> 24 void mergesort(Elem A[], Elem temp[], 25 int left, int right) { 26 if ((right-left) <= THRESHOLD) { 27 inssort<Elem>(&A[left],right-left+1); 28 return; 29 } 30 int i, j, k, mid = (left+right)/2; 31 if (left == right) return; 32 mergesort<Elem>(A, temp, left, mid); 33 mergesort<Elem>(A, temp, mid+1, right); 34 for (i=mid; i>=left; i--) temp[i] = A[i]; 35 for (j=1; j<=right-mid; j++) 36 temp[right-j+1] = A[j+mid]; 37 for (i=left,j=right,k=left; k<=right; k++) 38 if (temp[i] < temp[j]) A[k] = temp[i++]; 39 else A[k] = temp[j--];}
算法總的時間複雜度爲O(nlogn),該時間代價不依賴於待排序數組的相對順序,所以這是最佳、平均、最差時間。
歸併排序佔用附加存儲較多,須要另一個與原待排序對象數組一樣大小的輔助數組。這是這個算法的缺點。
非遞歸算法:
在內部排序中,一般採用的是2路歸併排序。即:將兩個位置相鄰的有序子序列歸併爲一個有序序列,這個操做對順序表而言,是垂手可得的。
1 void MergePass ( Element &L1, Element &L2, 2 int step, int Length ) {//一趟歸併排序 3 i =0; 4 while ( i+2* step <= Length-1 ){ 5 Merge ( L1, L2, i, i+ step -1, i+2* step -1); 6 i += 2 * step; 7 } 8 if ( i+ stepLen <= Length -1 ) 9 Merge ( L1, L2, i, i+ stepLen -1, Length -1 ); 10 else for ( j=i; j <= Length -1; j++ ) 11 L2[j] = L1[j]; 12 } 13 14 void MergeSort ( Element &list, int Length ) { 15 Element tempList[MaxSize]; 16 int step = 1; 17 while ( step < Length ) { 18 MergePass (list, tempList, step ); step*= 2; 19 MergePass (tempList, list, step ); step*= 2; 20 } 21 }
在一趟快速排序中,找一個記錄,以它的關鍵字做爲「樞軸(pivot)」,凡關鍵字小於樞軸的記錄均移動至該記錄以前,反之,凡關鍵字大於樞軸的記錄均移動至該記錄以後。
舉個栗子:
在調整過程當中,設立了兩個指針:low 和high,它們的初值分別爲:s 和 t, 以後逐漸減少 high,增長 low,並保證R[high].key≥52 和 R[low].key≤52,不然進行記錄的「交換」。
所以,在通過一次快排以後,序列變成{23, 49,14, 36,(52) 58,61, 97, 80, 75} ,此時pivot找到了本身的位置。
在算法導論裏面,快速排序選擇都是元素序列的最後一個元素,假設元素序列以下{3,9,5A,6,8,5B},這種狀況下,和上面的狀況一下,穩不穩定仍是看判斷的時候是否出現等號,可是若是選擇不是這樣的,咱們假設一種特殊情況:{3,9,5A,5B,6,8,5C},算法的實現是選擇中間的5B做爲中點,則不論等號與否,都是不穩定的。實際上,算法導論的選擇是很是有意義的,瞭解其算法過程的人能夠看到,這樣的選擇極大的下降了交換元素的複雜度和移動元素的次數。算法導論中是加了等號的,即≤最後一個元素的值被移到了左邊,於是快速排序是不穩定的。
1 template<typename E> 2 void qsort(E A[],int i,int j){ //第一次調用時的形式是qsort(array,0,n-1) 3 if(j<=i)return; 4 int pivotindex=findpivot(A,i,j); 5 swap(A,pivotindex,j); 6 int k=partition<E>(A,i-1,j,A[j]); 7 swap(A,k,j); 8 qsort<E>(A,i,k-1); 9 qsort<E>(A,k+1,j); 10 } 11 template<E> 12 inline int findpivot(E A[],int i,int j){ //規定樞軸 13 return (i+j)/2; 14 } 15 template<E> 16 inline int partition(E A[],int low,int high,E& pivot){ //每一輪進行劃分 17 while(low<high){ 18 while(A[high]>=pivot) 19 --high; 20 A[low]=A[high]; 21 while(A[low]<=pivot) 22 ++low; 23 A[high]=A[low]; 24 } 25 R[low]=R[0]; 26 return low; 27 }
若是每次劃分對一個對象定位後,若左側與右側子序列的長度相同,則下一步是對兩個長度減半的子序列進行排序,這是最理想的狀況。能夠證實,函數quicksort的平均計算時間也是o(nlogn)。實驗結果代表:就平均計算時間而言,快速排序是咱們所討論的全部內排序方法中最好的一個。在最壞的狀況,即待排序序列已經從小到大有序時,每次劃分只獲得一個比上一次少一個對象的子序列。這樣,必須通過n-1趟才能把全部對象定位,並且第i 趟須要通過n-i次比較,總的比較次數將達到n平方/2。排序速度退化到簡單排序的水平,比直接插入排序還慢。
對於 n較大的平均狀況而言,快速排序是「快速」的,可是當n很小時,這種排序方法比其它簡單排序方法還要慢。
若能更合理地選擇基準對象,使得每次劃分所得的兩個子序列中的對象個數儘量地接近,能夠加速排序速度,有一種改進辦法:取每一個待排序對象序列的第一個對象、最後一個對象和位置接近正中的3個對象,取其關鍵碼居中者做爲基準對象。
堆排序從小到大排序:首先將數組元素建成大小爲n的大頂堆,堆頂(數組第一個元素)是全部元素中的最大值,將堆頂元素和數組最後一個元素進行交換,再將除了最後一個數的n-1個元素創建成大頂堆,再將最大元素和數組倒數第二個元素進行交換,重複直至堆大小減爲1。
堆排序是不穩定的:好比:3 27 36 27,若是堆頂3先輸出,則,第三層的27(最後一個27)跑到堆頂,而後堆穩定,繼續輸出堆頂,是剛纔那個27,這樣說明後面的27先於第二個位置的27輸出,不穩定。
template <class Elem, class Comp> void heapsort(Elem A[], int n) { // Heapsort Elem mval; maxheap<Elem,Comp> H(A, n, n); for (int i=0; i<n; i++) // Now sort H.removemax(mval); // Put max at end }
堆排序的最佳、平均、最差時間代價是O(nlogn),在平均狀況下它比快速排序慢一個常數因子,可是堆排序更適合外排序,處理數據集太大而不適合在內存中排序的狀況。堆排序建堆很快,所以若是但願找數組中第k大的元素,能夠用o(n+klogn),若是k很小,它的速度就會比其餘算法快不少。
基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序,最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,因此其是穩定的排序算法。
在計算機上實現基數排序時,爲減小所需輔助存儲空間,應採用鏈表做存儲結構,即鏈式基數排序,具體做法爲:
1.待排序記錄以指針相鏈,構成一個鏈表;
2.「分配」 時,按當前「關鍵字位」所取值,將記錄分配到不一樣的 「鏈隊列」 中,每一個隊列中記錄的 「關鍵字位」 相同;
3.「收集」時,按當前關鍵字位取值從小到大將各隊列首尾相鏈成一個鏈表;
4.對每一個關鍵字位均重複2)和 3) 兩步。
1 template <class Type> 2 void RadixSort ( staticlinklist<Type> &list, 3 const int d, const int radix ) { 4 int rear[radix], front[radix], n=list.CurrentSize; 5 for ( int i = 1; i < n; i++ ) 6 list.Data[i].setLink(i+1); 7 list. Data[n].setLink(0); //靜態鏈表初始化 8 int current = 1; //鏈表掃描指針 9 for ( i = d-1; i >= 0; i-- ) { //作 d 趟分配.收集 10 for ( int j = 0; j < radix; j++ ) front[j] = 0; 11 while ( current != 0 ) { //逐個對象分配 12 int k = list.Data[current].getKey(i); 13 //取當前對象關鍵碼的第 i 位 14 if ( front[k] == 0) //原鏈表爲空,對象鏈入 15 front[k] = current; 16 else //原鏈表非空,鏈尾鏈入 17 list. Data[rear[k]].setLink (current); 18 rear[k] = current; //修改鏈尾指針 19 current = list. Data[current].getLink ( ); 20 } 21 j = 0; //從0號隊列開始收集 22 while ( front[j] == 0 ) j++; //空隊列跳過 23 current = front[j]; 24 list. Data[0].setLink (current); 25 int last = rear[j]; 26 for ( k = j+1; k < radix; k++) //逐個隊列連接 27 if ( front[k] ) { 28 list. Data[last].setLink(front[k]); 29 last = rear[k]; } 30 list. Data[last].setLink(0); }}
若每一個關鍵碼有d 位,須要重複執行d 趟「分配」與「收集」。每趟對n 個對象進行「分配」,對radix個隊列進行「收集」。總時間複雜度爲O( d ( n+radix))。
若基數radix相同,對於對象個數較多而關鍵碼位數較少的狀況,使用鏈式基數排序較好
基數排序須要增長n+2radix個附加連接指針
排序法 | 平均時間 | 最差情形 | 穩定度 | 額外空間 | 備註 |
---|---|---|---|---|---|
冒泡 | O(n2) | O(n2) | 穩定 | O(1) | n小時較好 |
交換 | O(n2) | O(n2) | 不穩定 | O(1) | n小時較好 |
選擇 | O(n2) | O(n2) | 不穩定 | O(1) | n小時較好 |
插入 | O(n2) | O(n2) | 穩定 | O(1) | 大部分已排序時較好 |
基數 | O(logRB) | O(logRB) | 穩定 | O(n) | B是真數(0-9), R是基數(個十百) |
Shell | O(nlogn) | O(ns) 1<s<2 | 不穩定 | O(1) | s是所選分組 |
快速 | O(nlogn) | O(n2) | 不穩定 | O(nlogn) | n大時較好 |
歸併 | O(nlogn) | O(nlogn) | 穩定 | O(1) | n大時較好 |
堆 | O(nlogn) | O(nlogn) | 不穩定 | O(1) | n大時較好 |
本文參考資料及學習資源:
1.排序算法總結(不少圖是從裏面搬過來的)http://www.javashuo.com/article/p-ppjzfxnr-hb.html
3.匈牙利 Sapientia 大學的 6 種排序算法舞蹈視頻,很是有創意http://top.jobbole.com/1539/
4.數據結構可視化:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
5.排序算法可視化 http://jsdo.it/norahiko/oxIy/fullscreen