前言
前段時間,我的一位鐘情.net的童鞋在編寫一套“教務管理系統”的時候,遇到了一個問題。因為系統中包含學生的成績排序,
而大學英語作為公用課有非常多人考試。這使得大學英語的成績記錄達到了二十多萬行記錄。排序起來非常耗時。整個系統還有
很多bug需要他處理,於是他就希望我能幫他解決這個問題。在寫代碼之前我先看了下.net的sort方法。msdn上寫道“此方
法使用 QuickSort 演算法。此實現執行不穩定排序;”。原來ms用的是快排!看來要想解決這個問題,一定不能用基於比較的
排序了。因為快排的平均時間複雜度已經達到了理論上限nlog(n)。通過分析待排資料,我發現這些資料有很多特點:
1.全部為整數(我們學校的考試成績沒有小數) 2.沒有負數(成績最低為0) 3.有範圍(0 ~ 100)
想必排序學的不錯的同學已經發現這些條件與計數排序對資料的要求一樣了吧~。而計數排序的時間複雜度達到了驚人的
O(n),換言之,計數排序只需要掃描待排序列一次就可以完成排序了。用這種排序輕鬆的解決了童鞋的問題。
這個故事的寓意:
你要知道,演算法真的很重要。按照需要選擇適當的演算法,這種能力是非常重要的,每個優秀的軟體開發人員都應該具備。並不需要
你能夠對演算法進行詳細的數學分析,但是你必須能夠理解這寫分析。並不需要你發明新的演算法,但是你要知道哪個演算法可以解決
手頭的問題。本文希望通過分析排序演算法協助你提高這種能力。當你具備這種能力後,你的軟體開發工具箱中又多了一件軟體開
發利器。
計數排序
想象一下,我們需要對一群孩子按照年齡的大小排序。已知孩子的年齡範圍為1~20。我們在地上畫20個圈代表1~20歲,讓
孩子們選擇符合自己年齡的圈站進去。當孩子站好後,排序就已經完成了。注意這個過程中並沒有用兩個孩子的年齡相互比較
。這種排序法就叫做計數排序。
演算法分析
利用地址位移來進行排序。排序位元組串、寬位元組串最快的排序演算法。而且實現及其簡單。
最好時間複雜度 O(n) 平均時間複雜度 O(n) 最壞時間複雜度 O(n) 穩定排序
優點
不需要比較函數。是對範圍固定在[0,K)的整數排序的最佳選擇。
缺點
需要一個size至少等於待排序數組取值範圍的緩衝區。條件苛刻,不能用作通用演算法。
演算法實現
void countsort(int * arr, int n, int k)//排序arr中n個元素,範圍是[0,K){ int i, idx = 0; int *B = calloc(k, sizeof(int)); for(i = 0; i < n; i++) B[arr[i]]++; for(i = 0; i < k; i++) while(B[i]-- > 0) arr[idx++] = i;
free(B);}
計數排序在位元組串排序中。表現也不錯。
void countsort(BYTE *array, int length){ int t; int i, z = 0; BYTE min,max; int *count; min = max = array[0]; for(i=0; i<length; i++) { if(array[i] < min) min = array[i]; else if(array[i] > max) max = array[i]; } count = (int*)malloc((max-min+1)*sizeof(int)); for(i=0; i<max-min+1; i++) count[i] = 0; for(i = 0; i < length; i++) count[array[i]-min]++; for(t = 0; t <= 255; t++) for(i = 0; i < count[t-min]; i++) array[z++] = (BYTE)t; free(count);}
桶排序
給定n個元素的集合,桶排序構造了n個桶來切分輸入集合因此同排序可以降低處理時額外空間的開銷。
演算法分析
想要使桶排序能夠在最壞的情況下也能夠以O(n)的時間複雜度進行排序,需要滿足兩個條件:
1.輸入的資料要均勻的分布在一個給定的範圍內。2.桶必須是有序的。
桶排序不適合排序隨機字串,但是在排序在區間[0,1)間的浮點數時,可以秒殺其他演算法。
最好時間複雜度 O(n) 平均時間複雜度 O(n) 最壞時間複雜度 O(n) 穩定排序
優點
在[0,1)區間內排序浮點數的最佳演算法。
缺點
需要額外的儲存空間。
演算法實現
//虛擬碼const int nBuckets = (MAX / 10) + 1;Bucket bucket [nBuckets];for (i = 0; i < n; i++) bucket [A[i] / 10].insert (A[i]);for (i = 0; i < nBuckets; i++) bucket [i].sort ();for (i = 0; i < nBuckets; i++) cout << bucket [i];
具體實現
#include <iostream.h>class element {public:int value;element *next;element(){value=NULL;next=NULL;}};class bucket {public:element *firstElement;bucket(){firstElement = NULL;}};void main() {int lowend=0; int highend=100; int interval=10; const int noBuckets=(highend-lowend)/interval;bucket *buckets=new bucket[noBuckets]; bucket *temp;for(int a=0;a<noBuckets;a++) {temp=new bucket;buckets[a]=*temp;}int array[]={12,2,22,33,44,55,66,77,85,87,81,83,89,82,88,86,84,88,99};for(int j=0;j<19;j++) {cout<<array[j]<<endl;element *temp,*pre;temp=buckets[array[j]/interval].firstElement;if(temp==NULL){temp=new element;buckets[array[j]/interval].firstElement=temp;temp->value=array[j];}else{pre=NULL;while(temp!=NULL) { if(temp->value>array[j]) break; pre=temp; temp=temp->next; }if(temp->value>array[j]) {if(pre==NULL) {element *firstNode;firstNode=new element();firstNode->value=array[j];firstNode->next=temp;buckets[array[j]/interval].firstElement=firstNode;}else{element *firstNode;firstNode=new element();firstNode->value=array[j];firstNode->next=temp;pre->next=firstNode;}}else{temp=new element;pre->next=temp;temp->value=array[j];}} }for(int jk=0;jk<10;jk++){element *temp;temp= buckets[jk].firstElement;while(temp!=NULL){cout<<"*"<<temp->value<<endl;temp=temp->next;}}}
堆排序
在數組A中尋找最大的元素最少需要n-1次比較,但是我們希望能夠最少化直接比較的元素。在一個比賽中,有64個隊伍參加。
那麼每次隊伍之需要贏log(64) = 6次比賽就可以就可以拿到冠軍的獎牌了。堆排序則說明了如何運用這種方法來排序元素。
演算法分析
一個堆就是一顆二叉樹。深度為k-1存在2^(k-1)個節點,節點是從左至右添加的。樹中每個節點的值都大於或者等於任意
一個子節點的值。一個嚴格滿足堆性質的結構,可以儲存在數組中,而不損失任何資料。
最好時間複雜度 O(nlogn) 平均時間複雜度 O(nlogn) 最壞時間複雜度 O(nlogn) 非穩定排序
優點
可以快速的排序出前n大的數,求前n個大(小)數效率很高。
缺點
實現相對複雜
演算法實現
#define MAX_HEAP_LEN 100 static int heap[MAX_HEAP_LEN]; static int heap_size = 0; // 堆的大小 static void swap(int* a, int* b) { int temp = 0; temp = *b; *b = *a; *a = temp; } static void shift_up(int i) { int done = 0; if( i == 0) return; //為根項目 while((i!=0)&&(!done)) { if(heap[i] > heap[(i-1)/2]) {//如果當前值大於父親節點,就交換 swap(&heap[i],&heap[(i-1)/2]); } else { done =1; } i = (i-1)/2; } } static void shift_down(int i) { int done = 0; if (2*i + 1> heap_size) return; while((2*i+1 < heap_size)&&(!done)) { i =2*i+1; if ((i+1< heap_size) && (heap[i+1] > heap[i])) { i++; } if (heap[(i-1)/2] < heap[i]) { swap(&heap[(i-1)/2], &heap[i]); } else { done = 1; } } } static void delete(int i) { int current = heap[i]; // 刪除 int last = heap[heap_size - 1]; // 擷取最後一個 heap_size--; if (i == heap_size) return; heap[i] = last; // 複製為最後一個元素,覆蓋當前值 if(last >= current) sift_up(i); else sift_down(i); } int delete_max() { int ret = heap[0]; delete(0); return ret; } void insert(int new_data) { if(heap_size >= MAX_HEAP_LEN) return; heap_size++; heap[heap_size - 1] = new_data; shift_up(heap_size - 1); }
快速排序
在快速排序中,我們通過某些策略(有時隨機,有時是最左邊,有時是中間)來選擇一個中間元素,這個元素將數組切分成兩個
子數組。左邊之數組的元素都小於或者等於中間元素,右邊子數組的元素都大於或者等於中間愛你元素。然後每個子數組被遞迴
地排序。
演算法分析
以平均效能來說,排序n個項目要O(nlogn)次比較。然而,在最壞的效能下,它需要O(n^2)次比較。一般來說,快速排序實際
上明顯地比其他O(nlogn) 演算法更快,因為它的內部迴圈可以在大部分的架構上很有效率地被實現出來。
最好時間複雜度 O(nlogn) 平均時間複雜度 O(nlogn) 最壞時間複雜度 O(n^2) 不穩定排序
優點
在平均情況下,快速排序確實名副其實,並且實現簡單。
缺點
標準的遞迴快速排序,需要額外的空間。平均情況下需要O(logn)的位元組空間,以及最壞情況下O(nlogn)的位元組空間
演算法實現
void swap(int *a, int *b){ int t=*a; *a=*b; *b=t; }void qsort(int arr[],int l,int r){ int i = l; int j = r; int key = arr[(i+j)/2]; while(i < j) { for(;(i < r)&&(arr[i] < key);i++); for(;(j > l)&&(arr[j] > key);j--); if (i <= j) { swap(&arr[i],&arr[j]); i++; j--; } } if (i < r) qsort(arr,i,r); if (j > l) qsort(arr,l,j);}