排序算法能夠說是一項基本功,解決實際問題中常常遇到,針對實際數據的特色選擇合適的排序算法可使程序得到更高的效率,有時候排序的穩定性仍是實際問題中必須考慮的,這篇博客對常見的排序算法進行整理,包括:插入排序、選擇排序、冒泡排序、快速排序、堆排序、歸併排序、希爾排序、二叉樹排序、計數排序、桶排序、基數排序。git
代碼都通過了CodeBlocks的調試,可是極可能有沒注意到的BUG,歡迎指出。算法
比較排序和非比較排序shell
常見的排序算法都是比較排序,非比較排序包括計數排序、桶排序和基數排序,非比較排序對數據有要求,由於數據自己包含了定位特徵,全部才能不經過比較來肯定元素的位置。編程
比較排序的時間複雜度一般爲O(n2)或者O(nlogn),比較排序的時間複雜度下界就是O(nlogn),而非比較排序的時間複雜度能夠達到O(n),可是都須要額外的空間開銷。數組
比較排序時間複雜度爲O(nlogn)的證實:數據結構
a1,a2,a3……an序列的全部排序有n!種,因此知足要求的排序a1',a2',a3'……an'(其中a1'<=a2'<=a3'……<=an')的機率爲1/n!。基於輸入元素的比較排序,每一次比較的返回不是0就是1,這剛好能夠做爲決策樹的一個決策將一個事件分紅兩個分支。好比冒泡排序時經過比較a1和a2兩個數的大小能夠把序列分紅a1,a2……an與a2,a1……an(氣泡a2上升一個身位)兩種不一樣的結果,所以比較排序也能夠構造決策樹。根節點表明原始序列a1,a2,a3……an,全部葉子節點都是這個序列的重排(共有n!個,其中有一個就是咱們排序的結果a1',a2',a3'……an')。若是每次比較的結果都是等機率的話(剛好劃分爲機率空間相等的兩個事件),那麼二叉樹就是高度平衡的,深度至少是log(n!)。ide
又由於 1. n! < nn ,兩邊取對數就獲得log(n!)<nlog(n),因此log(n!) = O(nlogn).
函數
2. n!=n(n-1)(n-2)(n-3)…1 > (n/2)^(n/2) 兩邊取對數獲得 log(n!) > (n/2)log(n/2) = Ω(nlogn),因此 log(n!) = Ω(nlogn)。優化
所以log(n!)的增加速度與 nlogn 相同,即 log(n!)=Θ(nlogn),這就是通用排序算法的最低時間複雜度O(nlogn)的依據。ui
排序的穩定性和複雜度
不穩定:
選擇排序(selection sort)— O(n2)
快速排序(quicksort)— O(nlogn) 平均時間, O(n2) 最壞狀況; 對於大的、亂序串列通常認爲是最快的已知排序
堆排序 (heapsort)— O(nlogn)
希爾排序 (shell sort)— O(nlogn)
基數排序(radix sort)— O(n·k); 須要 O(n) 額外存儲空間 (K爲特徵個數)
穩定:
插入排序(insertion sort)— O(n2)
冒泡排序(bubble sort) — O(n2)
歸併排序 (merge sort)— O(n log n); 須要 O(n) 額外存儲空間
二叉樹排序(Binary tree sort) — O(nlogn); 須要 O(n) 額外存儲空間
計數排序 (counting sort) — O(n+k); 須要 O(n+k) 額外存儲空間,k爲序列中Max-Min+1
桶排序 (bucket sort)— O(n); 須要 O(k) 額外存儲空間
每種排序的原理和實現
插入排序
遍歷數組,遍歷到i時,a0,a1...ai-1是已經排好序的,取出ai,從ai-1開始向前和每一個比較大小,若是小於,則將此位置元素向後移動,繼續先前比較,若是不小於,則放到正在比較的元素以後。可見相等元素比較是,原來靠後的仍是拍在後邊,因此插入排序是穩定的。
當待排序的數據基本有序時,插入排序的效率比較高,只須要進行不多的數據移動。
void insertion_sort (int a[], int n) { int i,j,v; for (i=1; i<n; i++) {
//若是第i個元素小於第j個,則第j個向後移動 for (v=a[i], j=i-1; j>=0&&v<a[j]; j--) a[j+1]=a[j]; a[j+1]=v; } }
選擇排序
遍歷數組,遍歷到i時,a0,a1...ai-1是已經排好序的,而後從i到n選擇出最小的,記錄下位置,若是不是第i個,則和第i個元素交換。此時第i個元素可能會排到相等元素以後,形成排序的不穩定。
void selection_sort (int a[], int n) { int i,j,pos,tmp; for (i=0; i<n-1; i++) {
//尋找最小值的下標 for (pos=i, j=i+1; j<n; j++) if (a[pos]>a[j]) pos=j; if (pos != i) { tmp=a[i]; a[i]=a[pos]; a[pos]=tmp; } } }
冒泡排序
冒泡排序的名字很形象,實際實現是相鄰兩節點進行比較,大的向後移一個,通過第一輪兩兩比較和移動,最大的元素移動到了最後,第二輪次大的位於倒數第二個,依次進行。這是最基本的冒泡排序,還能夠進行一些優化。
優化一:若是某一輪兩兩比較中沒有任何元素交換,這說明已經都排好序了,算法結束,可使用一個Flag作標記,默認爲false,若是發生交互則置爲true,每輪結束時檢測Flag,若是爲true則繼續,若是爲false則返回。
優化二:某一輪結束位置爲j,可是這一輪的最後一次交換髮生在lastSwap的位置,則lastSwap到j之間是排好序的,下一輪的結束點就沒必要是j--了,而直接到lastSwap便可,代碼以下:
void bubble_sort (int a[], int n) { int i, j, lastSwap, tmp; for (j=n-1; j>0; j=lastSwap) {
lastSwap=0; //每一輪要初始化爲0,防止某一輪未發生交換,lastSwap保留上一輪的值進入死循環 for (i=0; i<j; i++) { if (a[i] > a[i+1]) { tmp=a[i]; a[i]=a[i+1]; a[i+1]=tmp; //最後一次交換位置的座標 lastSwap = i; } } } }
快速排序
快速排序首先找到一個基準,下面程序以第一個元素做爲基準(pivot),而後先從右向左搜索,若是發現比pivot小,則和pivot交換,而後從左向右搜索,若是發現比pivot大,則和pivot交換,一直到左邊大於右邊,此時pivot左邊的都比它小,而右邊的都比它大,此時pivot的位置就是排好序後應該在的位置,此時pivot將數組劃分爲左右兩部分,能夠遞歸採用該方法進行。快排的交換使排序成爲不穩定的。
int mpartition(int a[], int l, int r) { int pivot = a[l]; while (l<r) { while (l<r && pivot<=a[r]) r--; if (l<r) a[l++]=a[r]; while (l<r && pivot>a[l]) l++; if (l<r) a[r--]=a[l]; } a[l]=pivot; return l; } void quick_sort (int a[], int l, int r) { if (l < r) { int q = mpartition(a, l, r); msort(a, l, q-1); msort(a, q+1, r); } }
堆排序
堆排序是把數組看做堆,第i個結點的孩子結點爲第2*i+1和2*i+2個結點(不超出數組長度前提下),堆排序的第一步是建堆,而後是取堆頂元素而後調整堆。建堆的過程是自底向上不斷調整達成的,這樣當調整某個結點時,其左節點和右結點已是知足條件的,此時若是兩個子結點不須要動,則整個子樹不須要動,若是調整,則父結點交換到子結點位置,再以此結點繼續調整。
下述代碼使用的大頂堆,創建好堆後堆頂元素爲最大值,此時取堆頂元素即便堆頂元素和最後一個元素交換,最大的元素處於數組最後,此時調整小了一個長度的堆,而後再取堆頂和倒數第二個元素交換,依次類推,完成數據的非遞減排序。
堆排序的主要時間花在初始建堆期間,建好堆後,堆這種數據結構以及它奇妙的特徵,使得找到數列中最大的數字這樣的操做只須要O(1)的時間複雜度,維護須要logn的時間複雜度。堆排序不適宜於記錄數較少的文件
void heapAdjust(int a[], int i, int nLength) { int nChild; int nTemp; for (nTemp = a[i]; 2 * i + 1 < nLength; i = nChild) { // 子結點的位置=2*(父結點位置)+ 1 nChild = 2 * i + 1; // 獲得子結點中較大的結點 if ( nChild < nLength-1 && a[nChild + 1] > a[nChild]) ++nChild; // 若是較大的子結點大於父結點那麼把較大的子結點往上移動,替換它的父結點 if (nTemp < a[nChild]) { a[i] = a[nChild]; a[nChild]= nTemp; } else // 不然退出循環 break; } } // 堆排序算法 void heap_sort(int a[],int length) { int tmp; // 調整序列的前半部分元素,調整完以後第一個元素是序列的最大的元素 //length/2-1是第一個非葉節點,此處"/"爲整除 for (int i = length / 2 - 1; i >= 0; --i) heapAdjust(a, i, length); // 從最後一個元素開始對序列進行調整,不斷的縮小調整的範圍直到第一個元素 for (int i = length - 1; i > 0; --i) { // 把第一個元素和當前的最後一個元素交換, // 保證當前的最後一個位置的元素都是在如今的這個序列之中最大的 /// Swap(&a[0], &a[i]); tmp = a[i]; a[i] = a[0]; a[0] = tmp; // 不斷縮小調整heap的範圍,每一次調整完畢保證第一個元素是當前序列的最大值 heapAdjust(a, 0, i); } }
歸併排序
歸併排序是採用分治法(Divide and Conquer)的一個很是典型的應用。首先考慮下如何將將二個有序數列合併。這個很是簡單,只要從比較二個數列的第一個數,誰小就先取誰,取了後就在對應數列中刪除這個數。而後再進行比較,若是有數列爲空,那直接將另外一個數列的數據依次取出便可。這須要將待排序序列中的全部記錄掃描一遍,所以耗費O(n)時間,而由徹底二叉樹的深度可知,整個歸併排序須要進行.logn.次,所以,總的時間複雜度爲O(nlogn)。
歸併排序在歸併過程當中需 要與原始記錄序列一樣數量的存儲空間存放歸併結果,所以空間複雜度爲O(n)。
歸併算法須要兩兩比較,不存在跳躍,所以歸併排序是一種穩定的排序算法。
void mergearray(int a[], int first, int mid, int last, int temp[]) { 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 merge_sort(int a[], int first, int last, int temp[]) { if (first < last) { int mid = (first + last) / 2; merge_sort(a, first, mid, temp); //左邊有序 merge_sort(a, mid + 1, last, temp); //右邊有序 mergearray(a, first, mid, last, temp); //再將二個有序數列合併 } }
有的地方看到在mergearray()合併有序數列時分配臨時數組,即每一步mergearray的結果存放的一個新的臨時數組裏,這樣會在遞歸中消耗大量的空間。所以作出小小的變化。只須要new一個臨時數組。後面的操做都共用這一個臨時數組。合併完後將臨時數組中排好序的部分寫回原數組。
歸併排序計算時間複雜度時能夠很容易的列出遞歸方程,也是計算時間複雜度的一種方法。
希爾排序
希爾排序是對插入排序的優化,基於如下兩個認識:1. 數據量較小時插入排序速度較快,由於n和n2差距很小;2. 數據基本有序時插入排序效率很高,由於比較和移動的數據量少。
所以,希爾排序的基本思想是將須要排序的序列劃分紅爲若干個較小的子序列,對子序列進行插入排序,經過則插入排序可以使得原來序列成爲基本有序。這樣經過對較小的序列進行插入排序,而後對基本有序的數列進行插入排序,可以提升插入排序算法的效率。
希爾排序的劃分子序列不是像歸併排序那種的二分,而是採用的叫作增量的技術,例若有十個元素的數組進行希爾排序,首先選擇增量爲10/2=5,此時第1個元素和第(1+5)個元素配對成子序列使用插入排序進行排序,第2和(2+5)個元素組成子序列,完成後增量繼續減半爲2,此時第1個元素、第(1+2)、第(1+4)、第(1+6)、第(1+8)個元素組成子序列進行插入排序。這種增量選擇方法的好處是可使數組總體均勻有序,儘量的減小比較和移動的次數,二分法中即便前一半數據有序,後一半中若是有比較小的數據,仍是會形成大量的比較和移動,所以這種增量的方法和插入排序的配合更佳。
希爾排序的時間複雜度和增量的選擇策略有關,上述增量方法形成希爾排序的不穩定性。
void shell_sort(int a[], int n) { int d, i, j, temp; //d爲增量 for(d = n/2;d >= 1;d = d/2) //增量遞減到1使完成排序 { for(i = d; i < n;i++) //插入排序的一輪 { temp = a[i]; for(j = i - d;(j >= 0) && (a[j] > temp);j = j-d) { a[j + d] = a[j]; } a[j + d] = temp; } } }
二叉樹排序
二叉樹排序法藉助了數據結構二叉排序樹,二叉排序數知足三個條件:(1)若左子樹不空,則左子樹上全部結點的值均小於它的根結點的值; (2)若右子樹不空,則右子樹上全部結點的值均大於它的根結點的值; (3)左、右子樹也分別爲二叉排序樹。根據這三個特色,用中序遍歷二叉樹獲得的結果就是排序的結果。
二叉樹排序法須要首先根據數據構建二叉排序樹,而後中序遍歷,排序時間複雜度爲O(nlogn),構建二叉樹須要額外的O(n)的存儲空間,有相同的元素是能夠設置排在後邊的放在右子樹,在中序變量的時候也會在後邊,因此二叉樹排序是穩定的。
在實現此算法的時候遇到不小的困難,指針參數在函數中沒法經過new賦值,後來採用取指針地址,而後函數設置BST** tree的方式解決。
int arr[] = {7, 8, 8, 9, 5, 16, 5, 3,56,21,34,15,42}; struct BST{ int number; //保存數組元素的值 struct BST* left; struct BST* right; }; void insertBST(BST** tree, int v) { if (*tree == NULL) { *tree = new BST; (*tree)->left=(*tree)->right=NULL; (*tree)->number=v; return; } if (v < (*tree)->number) insertBST(&((*tree)->left), v); else insertBST(&((*tree)->right), v); } void printResult(BST* tree) { if (tree == NULL) return; if (tree->left != NULL) printResult(tree->left); cout << tree->number << " "; if (tree->right != NULL) printResult(tree->right); } void createBST(BST** tree, int a[], int n) { *tree = NULL; for (int i=0; i<n; i++) insertBST(tree, a[i]); } int main() { int n = sizeof(arr)/sizeof(int); BST* root; createBST(&root, arr, n); printResult(root); }
計數排序
若是經過比較進行排序,那麼複雜度的下界是O(nlogn),可是若是數據自己有能夠利用的特徵,能夠不經過比較進行排序,就能使時間複雜度下降到O(n)。
計數排序要求待排序的數組元素都是 整數,有不少地方都要去是0-K的正整數,其實負整數也能夠經過都加一個偏移量解決的。
計數排序的思想是,考慮待排序數組中的某一個元素a,若是數組中比a小的元素有s個,那麼a在最終排好序的數組中的位置將會是s+1,如何知道比a小的元素有多少個,確定不是經過比較去以爲,而是經過數字自己的屬性,即累加數組中最小值到a之間的每一個數字出現的次數(未出現則爲0),而每一個數字出現的次數能夠經過掃描一遍數組得到。
計數排序的步驟:
如下代碼中尋找最大和最小元素參考編程之美,比較次數爲1.5n次。
計數排序適合數據分佈集中的排序,若是數據太分散,會形成空間的大量浪費,假設數據爲(1,2,3,1000000),這就須要1000000的額外空間,而且有大量的空間浪費和時間浪費。
void findArrMaxMin(int a[], int size, int *min, int *max) { if(size == 0) { return; } if(size == 1) { *min = *max = a[0]; return; } *min = a[0] > a[1] ? a[1] : a[0]; *max = a[0] <= a[1] ? a[1] : a[0]; int i, j; for(i = 2, j = 3; i < size, j < size; i += 2, j += 2) { int tempmax = a[i] >= a[j] ? a[i] : a[j]; int tempmin = a[i] < a[j] ? a[i] : a[j]; if(tempmax > *max) *max = tempmax; if(tempmin < *min) *min = tempmin; } //若是數組元素是奇數個,那麼最後一個元素在分組的過程當中沒有包含其中, //這裏單獨比較 if(size % 2 != 0) { if(a[size -1] > *max) *max = a[size - 1]; else if(a[size -1] < *min) *min = a[size -1]; } } void count_sort(int a[], int b[], int n) { int max, min; findArrMaxMin(a, n, &min, &max); int numRange = max-min+1; int* counter = new int[numRange]; int i, j, k; for (k=0; k<numRange; k++) counter[k]=0; for (i=0; i<n; i++) counter[a[i]-min]++; for (k=1; k<numRange; k++) counter[k] += counter[k-1]; for (j=n-1; j>=0; j--) { int v = a[j]; int index = counter[v-min]-1; b[index]=v; counter[v-min]--; } }
桶排序
假設有一組長度爲N的待排關鍵字序列K[1....n]。首先將這個序列劃分紅M個的子區間(桶) 。而後基於某種映射函數 ,將待排序列的關鍵字k映射到第i個桶中(即桶數組B的下標 i) ,那麼該關鍵字k就做爲B[i]中的元素(每一個桶B[i]都是一組大小爲N/M的序列)。接着對每一個桶B[i]中的全部元素進行比較排序(可使用快排)。而後依次枚舉輸出B[0]....B[M]中的所有內容便是一個有序序列。
桶排序利用函數的映射關係,減小了計劃全部的比較操做,是一種Hash的思想,能夠用在海量數據處理中。
我以爲計數排序也能夠看做是桶排序的特例,數組關鍵字範圍爲N,劃分爲N個桶。
基數排序
基數排序也能夠看做一種桶排序,不斷的使用不一樣的標準對數據劃分到桶中,最終實現有序。基數排序的思想是對數據選擇多種基數,對每一種基數依次使用桶排序。
基數排序的步驟:以整數爲例,將整數按十進制位劃分,從低位到高位執行如下過程。
1. 從個位開始,根據0~9的值將數據分到10個桶桶,例如12會劃分到2號桶中。
2. 將0~9的10個桶中的數據順序放回到數組中。
重複上述過程,一直到最高位。
上述方法稱爲LSD(Least significant digital),還能夠從高位到低位,稱爲MSD。
int getNumInPos(int num,int pos) //得到某個數字的第pos位的值 { int temp = 1; for (int i = 0; i < pos - 1; i++) temp *= 10; return (num / temp) % 10; } #define RADIX_10 10 //十個桶,表示每一位的十個數字 #define KEYNUM 5 //整數位數 void radix_sort(int* pDataArray, int iDataNum) { int *radixArrays[RADIX_10]; //分別爲0~9的序列空間 for (int i = 0; i < RADIX_10; i++) { radixArrays[i] = new int[iDataNum]; radixArrays[i][0] = 0; //index爲0處記錄這組數據的個數 } for (int pos = 1; pos <= KEYNUM; pos++) //從個位開始到31位 { for (int i = 0; i < iDataNum; i++) //分配過程 { int num = getNumInPos(pDataArray[i], pos); int index = ++radixArrays[num][0]; radixArrays[num][index] = pDataArray[i]; } for (int i = 0, j =0; i < RADIX_10; i++) //寫回到原數組中,復位radixArrays { for (int k = 1; k <= radixArrays[i][0]; k++) pDataArray[j++] = radixArrays[i][k]; radixArrays[i][0] = 0; } } }