前面三篇博文咱們分別回顧了冒泡排序、選擇排序、插入排序、希爾排序、歸併排序、堆排序和快速排序。關於排序算法有幾種分類標準,穩定與非穩定、內部與外部。
所謂穩定的排序算法,意思是若是待排序序列有相同元素,通過排序算法處理後他們的相對順序和排序前在序列裏的相對順序同樣,這樣咱們就稱該排序算法是穩定;不然就是非穩定的。
所謂內部排序算法,意思是待排序序列數據量規模較小,排序直接在內存裏就能夠完成的排序算法;而外部排序是針對數據量特別大,不能一次性將全部數據調入內存來,在排序過程當中要不斷地訪問外部存儲設備的排序算法。咱們這裏介紹的七種排序算法,還有一個沒有介紹的基數排序,它們都是內部排序算法。
下面咱們用實際數據來測試一下這幾種算法的性能。經過前面幾篇博文的複習,我已經將這七種排序算法寫成了一個單獨的工程:
頭文件innersort.h:
- /**********************************************
- filename: innersort.h
- **********************************************/
- #include <stdlib.h>
- #include <string.h>
- #include <stdio.h>
-
- void bubble_sort(int a[],int len);
- void select_sort(int a[],int len);
- void insert_sort(int a[],int len);
- void shell_sort(int a[],int len);
- void merge_sort(int a[],int len);
- void heap_sort(int a[],int len);
- void quick_sort(int a[],int low,int high);
源文件innersort.c:
- /******************************************
- filename:innersort.c
- ******************************************/
- #include "innersort.h"
-
- //交換兩個數
- void swap(int *a,int *b)
- {
- int t;
- t = *a;
- *a = *b;
- *b = t;
- }
-
- //冒泡排序
- void bubble_sort(int a[],int len)
- {
- int i,goon;
- goon = 1;
- while(goon && len--){
- goon = 0;
- for(i=0;i<len;i++){
- if(a[i]>a[i+1]){
- swap(&a[i],&a[i+1]);
- goon =1;
- }
- }
- }
- }
-
- //選擇排序
- void select_sort(int a[],int len)
- {
- int i,j,min;
- for(i=0;i<len-1;i++){
- min = i;
- for(j=i+1;j<len;j++)
- if(a[min]>a[j])
- min = j;
- if(min != i){
- swap(&a[i],&a[min]);
- }
- }
- }
-
- //插入排序
- void insert_sort(int a[],int len)
- {
- int i,j,tmp;
- for(i=1;i<len;i++){
- for(j=i,tmp=a[i];j>0 && tmp < a[j-1];j--){
- a[j] = a[j-1];
- }
- a[j] = tmp;
- }
- }
-
- //希爾排序
- void shell_sort(int a[],int len)
- {
- int i,j,tmp,d=len;
- while((d/=2)>0){
- for(i=d;i<len;i++){
- for(j=i,tmp=a[i];j>=d && tmp < a[j-d];j-=d){
- a[j] = a[j-d];
- }
- a[j] = tmp;
- }
- }
- }
-
- //歸併操做,被歸併排序使用
- inline void merge_ops(int a[],int alen,int b[],int blen)
- {
- int i,j,k,len=alen+blen;
- int *tmp = (int*)malloc(sizeof(int)*len);
-
- i=j=k=0;
- while(i<alen && j<blen){
- tmp[k++] = ((a[i]<b[j]) ? a[i++]:b[j++]);
- }
-
- if(i>=alen && j<blen){
- memcpy(tmp+k,b+j,sizeof(int)*(blen-j));
- }
- if(j>=blen && i<alen){
- memcpy(tmp+k,a+i,sizeof(int)*(alen-i));
- }
- memcpy(a,tmp,sizeof(int)*len);
- free(tmp);
- }
-
- //歸併排序
- void merge_sort(int a[],int len)
- {
- if(len == 1){
- return;
- }
- merge_sort(a,len/2);
- merge_sort(a+len/2,len-len/2);
- merge_ops(a,len/2,a+len/2,len-len/2);
- }
-
- //用於堆排序,計算節點i的左子節點
- inline int leftChildIndex(int i)
- {
- return (2*i+1);
- }
-
- //用於堆排序,計算節點i的右子節點
- inline int rightChildIndex(int i)
- {
- return (2*i+2);
- }
-
- //將堆調整成大根堆的元操做函數
- inline void adjustHeap(int a[],int len,int i)
- {
- int l,r,bigger;
- l = leftChildIndex(i);
- r = rightChildIndex(i);
-
- while(l<len || r<len){
- if(r<len){
- bigger = ((a[l]>a[r])?l:r);
- }else if(l<len){
- bigger = l;
- }else{
- break;
- }
- if(a[bigger]>a[i]){
- swap(&a[i],&a[bigger]);
- i = bigger;
- l = leftChildIndex(i);
- r = rightChildIndex(i);
- }else
- break;
- }
- }
-
- //創建大根堆
- inline void buildHeap(int a[],int len)
- {
- int i;
- for(i=len/2-1;i>=0;i--){
- adjustHeap(a,len,i);
- }
- }
-
- //堆排序
- void heap_sort(int a[],int len)
- {
- int i;
- buildHeap(a,len);
-
- while(--len > 0){
- swap(&a[0],&a[len]);
- adjustHeap(a,len,0);
- }
- }
-
- //快速排序中用於拆分子序列的操做接口
- inline int partoff(int a[],int low,int high)
- {
- int key = a[low];
- while(low<high)
- {
- while(low<high&&key<=a[high])
- high--;
- if(low<high)
- a[low++] = a[high];
-
- while(low<high && key >= a[low])
- low++;
- if(low<high)
- a[high--] = a[low];
- }
- a[low] = key;
- return low;
- }
-
- //快速排序
- void quick_sort(int a[],int low,int high)
- {
- int index=0;
- if(low<high)
- {
- index = partoff(a,low,high);
- quick_sort(a,low,index-1);
- quick_sort(a,index+1,high);
- }
- }
關於測量函數執行時間有不少方式,clock(), times(), gettimeofday(), getrusage()等,還有經過編譯程序時,打開gcc的-pg選項,而後用gprof來測量,下面是我在網上找到的一個計算函數執行時間的版本,很是感謝博客園的「
靜心盡力」朋友,稍加改造一下,咱們就能夠經過編譯時給Makefile傳遞不一樣的宏選項,打開不一樣的時間測量方式:
- /*****************************************************
- filename: common.h
- 若是定義了TEST_BY_CLOCK,則採用clock()方式計量函數的執行時間;
- 若是定義了TEST_BY_TIMES,則採用times()方式計量函數的執行時間;
- 若是定義了TEST_BY_GETTIMEOFDAY,則採用gettimeofday()方式計量函數的執行時間;
- 若是定義了TEST_BY_GETRUSAGE,則採用getrusage()方式計量函數的執行時間;
- *****************************************************/
- #include <sys/time.h>
- #include <sys/resource.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <time.h>
- #include <stdlib.h>
- #include <string.h>
-
- //用於生成隨機待排序序列
- #define random(x) (rand()%x)
-
- static clock_t clockT1, clockT2;
- static double doubleT1, doubleT2;
-
- //非快速排序的統一回調測試接口
- typedef void (*sfun)(int a[],int len);
- //快速排序的測試接口
- typedef void (*sfun2)(int a[],int low,int high);
-
- /***************************************************
- 功能說明:生成隨機待排序序列
- 輸入參數:len-隨機序列長度,range-隨機序列裏元素的取值範圍
- 輸出參數:無
- 返 回 值:隨機序列首地址
- ***************************************************/
- int *genArray(int len,int range)
- {
- int i = 0;
- int *p = (int*)malloc(sizeof(int)*len);
- if(NULL == p)
- return NULL;
- srand((int)time(0));
- for(i=0;i<len;i++){
- p[i] = random(range);
- }
- return p;
- }
-
- /***************************************************
- 功能說明:逐次打印給定序列裏的每個元素
- 輸入參數:title-提示符,a-序列首地址,len-序列長度
- 輸出參數:無
- 返 回 值:無
- ***************************************************/
- void printforeach(char *title,int a[],int len)
- {
- int i = 0;
- printf("%s: ",title);
- for(i=0;i<len;i++){
- printf("%d ",a[i]);
- }
- printf("\n");
- }
-
- double getTimeval()
- {
- struct rusage stRusage;
- struct timeval stTimeval;
- #ifdef TEST_BY_GETTIMEOFDAY
- gettimeofday(&stTimeval, NULL);
- #endif
-
- #ifdef TEST_BY_GETRUSAGE
- getrusage(RUSAGE_SELF, &stRusage);
- stTimeval = stRusage.ru_utime;
- #endif
- return stTimeval.tv_sec + (double)stTimeval.tv_usec*1E-6;
- }
-
- void start_check(){
- #ifdef TEST_BY_CLOCK
- clockT1 = clock();
- #endif
-
- #ifdef TEST_BY_TIMES
- times(&clockT1);
- #endif
-
- #ifdef TEST_BY_GETTIMEOFDAY
- doubleT1 = getTimeval();
- #endif
-
- #ifdef TEST_BY_GETRUSAGE
- doubleT1 = getTimeval();
- #endif
- }
-
- void end_check(){
- #ifdef TEST_BY_CLOCK
- clockT2 = clock();
- printf("Time result tested by clock = %10.30f\n",
- (double)(clockT2 - clockT1)/CLOCKS_PER_SEC);
- #endif
-
- #ifdef TEST_BY_TIMES
- times(&clockT2);
- printf("Time result tested by times = %10.30f\n",
- (double)(clockT2 - clockT1)/sysconf(_SC_CLK_TCK));
- #endif
-
- #ifdef TEST_BY_GETTIMEOFDAY
- doubleT2 = getTimeval();
- printf("Time result tested by gettimeofday = %10.30f\n",
- (double)(doubleT2 - doubleT1));
- #endif
-
- #ifdef TEST_BY_GETRUSAGE
- doubleT2 = getTimeval();
- printf("Time result tested by getrusage = %10.70f\n",
- (double)(doubleT2 - doubleT1));
- #endif
- }
-
- void do_test(sfun fun_ptr,int a[],int len){
- start_check();
- (*fun_ptr)(a,len);
- end_check();
- }
-
- void do_test2(sfun2 fun_ptr,int a[],int low,int high){
- start_check();
- (*fun_ptr)(a,low,high);
- end_check();
- }
最終的測試代碼以下:
- #include "common.h"
- #include "innersort.h"
-
- #ifdef NOECHO
- #define printforeach(...) {}
- #endif
-
- int main(int argc,char** argv){
- if(3 != argc){
- printf("Usage: %s total range \n",argv[0]);
- return 0;
- }
- int len = atoi(argv[1]);
- int range = atoi(argv[2]);
-
- int *p = genArray(len,range);
- int *data = (int*)malloc(sizeof(int)*len);
-
- memcpy(data,p,4*len);
- printforeach("Pop before",data,len);
- do_test(bubble_sort,data,len);
- printforeach("Pop after ",data,len);
-
- memcpy(data,p,4*len);
- printforeach("select before",data,len);
- do_test(select_sort,data,len);
- printforeach("select after ",data,len);
-
- memcpy(data,p,4*len);
- printforeach("Insert before",data,len);
- do_test(insert_sort,data,len);
- printforeach("Insert after ",data,len);
-
- memcpy(data,p,4*len);
- printforeach("Shell before",data,len);
- do_test(shell_sort,data,len);
- printforeach("Shell after ",data,len);
-
- memcpy(data,p,4*len);
- printforeach("merge before",data,len);
- do_test(merge_sort,data,len);
- printforeach("merge after ",data,len);
-
- memcpy(data,p,4*len);
- printforeach("heap before",data,len);
- do_test(heap_sort,data,len);
- printforeach("heap after ",data,len);
-
- memcpy(data,p,4*len);
- printforeach("quick before",data,len);
- do_test2(quick_sort,data,0,len-1);
- printforeach("quick after ",data,len);
-
- free(p);
- free(data);
- return 0;
- }
Makefile文件的長相以下:
- TARGET = test
- SRC = test.c innersort.c
- OBJS = $(SRC:.c=.o)
- CC = gcc
- DEBUG += -pg
- INCLUDE = -I.
-
- all:$(TARGET)
- $(TARGET):$(OBJS)
- $(CC) $(INCLUDE) $(DEBUG) $(CFLAGS) $(OBJS) -o $(TARGET)
-
- %.o : %.c
- $(CC) $(INCLUDE) $(DEBUG) $(CFLAGS) -c $<
- clean:
- rm -fr $(TARGET) *.out $(OBJS)
最終,測試工程文件夾下的文件結構列表:
若是要關閉排序先後序列的輸出信息,則執行「make CFLAGS+="-DNOECHO"」,須要採用gettimeofday()來計量函數的實行時間,則執行「make CFLAGS+="-DTIME_BY_GETTIMEOFDAY
-DNOECHO"」;同理須要用clock()來計量,則將TIME_BY_GETTIMEOFDAY替換成TEST_BY_CLOCK。一次測試結果以下:
在數據量很小的狀況下希爾排序的性能要比快速排序稍微好一點點,可是當數據上量級別後,在七種內部排序算法裏,通過100次測試後發現,快速排序的性能絕對是最優的:
(測試環境:CPU-AMD 速龍雙核2.1GHz,內存-2G,操做系統-Fedora 17,內核版本-3.3.4)
在下面的對比圖裏咱們能夠看到,當數據量上10萬後,冒泡排序算法明顯力不從心了,選擇排序和插入排序性能至關,但也有點不可接受。可是當數據量達到百萬後前三種算法已經跑不出結果了,但快速排序和歸併排序算法排列一百萬條數只需不到1秒鐘的時間。當數據量達到一千萬時,快速排序也只需3.8秒左右。因此,結論已經很明顯了。
固然,上述是我用gettimeofday()測量出的算法性能,感興趣的朋友還能夠用其它幾種方式,或者再對比一下gprof的統計結果,看看快速排序究竟是不是真漢子。 這四篇博文是比較簡單的筆記,也僅複習了常見的幾種內部排序,外部排序算法還有其餘新的算法都沒有涉及,有機會再補充。