经典排序算法归纳笔记(4)

9520阅读 3评论2014-05-04 wjlkoorey258
分类:C/C++

    前面三篇博文我们分别回顾了冒泡排序、选择排序、插入排序、希尔排序、归并排序、堆排序和快速排序。关于排序算法有几种分类标准,稳定与非稳定、内部与外部。

   所谓稳定的排序算法,意思是如果待排序序列有相同元素,经过排序算法处理后他们的相对顺序和排序前在序列里的相对顺序一样,这样我们就称该排序算法是稳定;否则就是非稳定的。

   所谓内部排序算法,意思是待排序序列数据量规模较小,排序直接在内存里就可以完成的排序算法;而外部排序是针对数据量特别大,不能一次性将所有数据调入内存来,在排序过程中要不断地访问外部存储设备的排序算法。我们这里介绍的七种排序算法,还有一个没有介绍的基数排序,它们都是内部排序算法。

   下面我们用实际数据来测试一下这几种算法的性能。通过前面几篇博文的复习,我已经将这七种排序算法写成了一个单独的工程:
   头文件innersort.h:

点击(此处)折叠或打开

  1. /**********************************************
  2.  filename: innersort.h
  3.  **********************************************/
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <stdio.h>

  7. void bubble_sort(int a[],int len);
  8. void select_sort(int a[],int len);
  9. void insert_sort(int a[],int len);
  10. void shell_sort(int a[],int len);
  11. void merge_sort(int a[],int len);
  12. void heap_sort(int a[],int len);
  13. void quick_sort(int a[],int low,int high);
    源文件innersort.c:

点击(此处)折叠或打开

  1. /******************************************
  2.  filename:innersort.c
  3.  ******************************************/
  4. #include "innersort.h"

  5. //交换两个数
  6. void swap(int *a,int *b)
  7. {
  8.     int t;
  9.     t = *a;
  10.     *a = *b;
  11.     *b = t;
  12. }

  13. //冒泡排序
  14. void bubble_sort(int a[],int len)
  15. {
  16.     int i,goon;
  17.     goon = 1;
  18.     while(goon && len--){
  19.         goon = 0;
  20.         for(i=0;i<len;i++){
  21.             if(a[i]>a[i+1]){
  22.                 swap(&a[i],&a[i+1]);
  23.                 goon =1;
  24.             }
  25.         }
  26.     }
  27. }

  28. //选择排序
  29. void select_sort(int a[],int len)
  30. {
  31.     int i,j,min;
  32.     for(i=0;i<len-1;i++){
  33.         min = i;
  34.         for(j=i+1;j<len;j++)
  35.             if(a[min]>a[j])
  36.                 min = j;
  37.         if(min != i){
  38.             swap(&a[i],&a[min]);
  39.         }
  40.     }
  41. }

  42. //插入排序
  43. void insert_sort(int a[],int len)
  44. {
  45.     int i,j,tmp;
  46.     for(i=1;i<len;i++){
  47.         for(j=i,tmp=a[i];j>0 && tmp < a[j-1];j--){
  48.             a[j] = a[j-1];
  49.         }
  50.         a[j] = tmp;
  51.     }
  52. }

  53. //希尔排序
  54. void shell_sort(int a[],int len)
  55. {
  56.     int i,j,tmp,d=len;
  57.     while((d/=2)>0){
  58.         for(i=d;i<len;i++){
  59.             for(j=i,tmp=a[i];j>=d && tmp < a[j-d];j-=d){
  60.                 a[j] = a[j-d];
  61.             }
  62.             a[j] = tmp;
  63.         }
  64.     }
  65. }

  66. //归并操作,被归并排序使用
  67. inline void merge_ops(int a[],int alen,int b[],int blen)
  68. {
  69.         int i,j,k,len=alen+blen;
  70.         int *tmp = (int*)malloc(sizeof(int)*len);

  71.         i=j=k=0;
  72.         while(i<alen && j<blen){
  73.             tmp[k++] = ((a[i]<b[j]) ? a[i++]:b[j++]);
  74.         }

  75.         if(i>=alen && j<blen){
  76.             memcpy(tmp+k,b+j,sizeof(int)*(blen-j));
  77.         }
  78.         if(j>=blen && i<alen){
  79.             memcpy(tmp+k,a+i,sizeof(int)*(alen-i));
  80.         }
  81.         memcpy(a,tmp,sizeof(int)*len);
  82.         free(tmp);
  83. }

  84. //归并排序
  85. void merge_sort(int a[],int len)
  86. {
  87.         if(len == 1){
  88.                 return;
  89.         }
  90.         merge_sort(a,len/2);
  91.         merge_sort(a+len/2,len-len/2);
  92.         merge_ops(a,len/2,a+len/2,len-len/2);
  93. }

  94. //用于堆排序,计算节点i的左子节点
  95. inline int leftChildIndex(int i)
  96. {
  97.         return (2*i+1);
  98. }

  99. //用于堆排序,计算节点i的右子节点
  100. inline int rightChildIndex(int i)
  101. {
  102.         return (2*i+2);
  103. }

  104. //将堆调整成大根堆的元操作函数
  105. inline void adjustHeap(int a[],int len,int i)
  106. {
  107.         int l,r,bigger;
  108.         l = leftChildIndex(i);
  109.         r = rightChildIndex(i);

  110.         while(l<len || r<len){
  111.                 if(r<len){
  112.                     bigger = ((a[l]>a[r])?l:r);
  113.                 }else if(l<len){
  114.                     bigger = l;
  115.                 }else{
  116.                     break;
  117.                 }
  118.                 if(a[bigger]>a[i]){
  119.                     swap(&a[i],&a[bigger]);
  120.                     i = bigger;
  121.                     l = leftChildIndex(i);
  122.                     r = rightChildIndex(i);
  123.                 }else
  124.                     break;
  125.         }
  126. }

  127. //建立大根堆
  128. inline void buildHeap(int a[],int len)
  129. {
  130.         int i;
  131.         for(i=len/2-1;i>=0;i--){
  132.             adjustHeap(a,len,i);
  133.         }
  134. }

  135. //堆排序
  136. void heap_sort(int a[],int len)
  137. {
  138.         int i;
  139.         buildHeap(a,len);

  140.         while(--len > 0){
  141.             swap(&a[0],&a[len]);
  142.             adjustHeap(a,len,0);
  143.         }
  144. }

  145. //快速排序中用于拆分子序列的操作接口
  146. inline int partoff(int a[],int low,int high)
  147. {
  148.         int key = a[low];
  149.         while(low<high)
  150.         {
  151.                 while(low<high&&key<=a[high])
  152.                         high--;
  153.                 if(low<high)
  154.                         a[low++] = a[high];

  155.                 while(low<high && key >= a[low])
  156.                         low++;
  157.                 if(low<high)
  158.                         a[high--] = a[low];
  159.         }
  160.         a[low] = key;
  161.         return low;
  162. }

  163. //快速排序
  164. void quick_sort(int a[],int low,int high)
  165. {
  166.     int index=0;
  167.     if(low<high)
  168.     {
  169.         index = partoff(a,low,high);
  170.         quick_sort(a,low,index-1);
  171.         quick_sort(a,index+1,high);
  172.     }
  173. }

    关于测量函数执行时间有很多方式,clock(), times(), gettimeofday(), getrusage()等,还有通过编译程序时,打开gcc的-pg选项,然后用gprof来测量,下面是我在网上找到的一个计算函数执行时间的版本,非常感谢博客园的“静心尽力”朋友,稍加改造一下,我们就可以通过编译时给Makefile传递不同的宏选项,打开不同的时间测量方式:

点击(此处)折叠或打开

  1. /*****************************************************
  2.  filename: common.h
  3.             如果定义了TEST_BY_CLOCK,则采用clock()方式计量函数的执行时间;
  4.             如果定义了TEST_BY_TIMES,则采用times()方式计量函数的执行时间;
  5.             如果定义了TEST_BY_GETTIMEOFDAY,则采用gettimeofday()方式计量函数的执行时间;
  6.             如果定义了TEST_BY_GETRUSAGE,则采用getrusage()方式计量函数的执行时间;
  7.  *****************************************************/
  8. #include <sys/time.h>
  9. #include <sys/resource.h>
  10. #include <unistd.h>
  11. #include <stdio.h>
  12. #include <time.h>
  13. #include <stdlib.h>
  14. #include <string.h>

  15. //用于生成随机待排序序列
  16. #define random(x) (rand()%x)

  17. static clock_t clockT1, clockT2;
  18. static double doubleT1, doubleT2;

  19. //非快速排序的统一回调测试接口
  20. typedef void (*sfun)(int a[],int len);
  21. //快速排序的测试接口
  22. typedef void (*sfun2)(int a[],int low,int high);

  23. /***************************************************
  24.  功能说明:生成随机待排序序列
  25.  输入参数:len-随机序列长度,range-随机序列里元素的取值范围
  26.  输出参数:无
  27.  返 回 值:随机序列首地址
  28.  ***************************************************/
  29. int *genArray(int len,int range)
  30. {
  31.     int i = 0;
  32.     int *p = (int*)malloc(sizeof(int)*len);
  33.     if(NULL == p)
  34.         return NULL;
  35.     srand((int)time(0));
  36.     for(i=0;i<len;i++){
  37.         p[i] = random(range);
  38.     }
  39.     return p;
  40. }

  41. /***************************************************
  42.  功能说明:逐次打印给定序列里的每一个元素
  43.  输入参数:title-提示符,a-序列首地址,len-序列长度
  44.  输出参数:无
  45.  返 回 值:无
  46.  ***************************************************/
  47. void printforeach(char *title,int a[],int len)
  48. {
  49.     int i = 0;
  50.     printf("%s: ",title);
  51.     for(i=0;i<len;i++){
  52.         printf("%d ",a[i]);
  53.     }
  54.     printf("\n");
  55. }
  56.  
  57. double getTimeval()
  58. {
  59.     struct rusage stRusage;
  60.     struct timeval stTimeval;
  61. #ifdef TEST_BY_GETTIMEOFDAY
  62.     gettimeofday(&stTimeval, NULL);
  63. #endif

  64. #ifdef TEST_BY_GETRUSAGE
  65.     getrusage(RUSAGE_SELF, &stRusage);
  66.     stTimeval = stRusage.ru_utime;
  67. #endif
  68.     return stTimeval.tv_sec + (double)stTimeval.tv_usec*1E-6;
  69. }
  70.  
  71. void start_check(){
  72. #ifdef TEST_BY_CLOCK
  73.     clockT1 = clock();
  74. #endif

  75. #ifdef TEST_BY_TIMES
  76.     times(&clockT1);
  77. #endif

  78. #ifdef TEST_BY_GETTIMEOFDAY
  79.     doubleT1 = getTimeval();
  80. #endif

  81. #ifdef TEST_BY_GETRUSAGE
  82.     doubleT1 = getTimeval();
  83. #endif
  84. }

  85. void end_check(){
  86. #ifdef TEST_BY_CLOCK
  87.     clockT2 = clock();
  88.     printf("Time result tested by clock = %10.30f\n",
  89.             (double)(clockT2 - clockT1)/CLOCKS_PER_SEC);
  90. #endif

  91. #ifdef TEST_BY_TIMES
  92.     times(&clockT2);
  93.     printf("Time result tested by times = %10.30f\n",
  94.             (double)(clockT2 - clockT1)/sysconf(_SC_CLK_TCK));
  95. #endif

  96. #ifdef TEST_BY_GETTIMEOFDAY
  97.     doubleT2 = getTimeval();
  98.     printf("Time result tested by gettimeofday = %10.30f\n",
  99.             (double)(doubleT2 - doubleT1));
  100. #endif

  101. #ifdef TEST_BY_GETRUSAGE
  102.     doubleT2 = getTimeval();
  103.     printf("Time result tested by getrusage = %10.70f\n",
  104.             (double)(doubleT2 - doubleT1));
  105. #endif
  106. }

  107. void do_test(sfun fun_ptr,int a[],int len){
  108.     start_check();
  109.     (*fun_ptr)(a,len);
  110.     end_check();        
  111. }

  112. void do_test2(sfun2 fun_ptr,int a[],int low,int high){
  113.     start_check();
  114.     (*fun_ptr)(a,low,high);
  115.     end_check();        
  116. }
    最终的测试代码如下:

点击(此处)折叠或打开

  1. #include "common.h"
  2. #include "innersort.h"

  3. #ifdef NOECHO
  4. #define printforeach(...) {}
  5. #endif

  6. int main(int argc,char** argv){
  7.     if(3 != argc){
  8.         printf("Usage: %s total range \n",argv[0]);
  9.         return 0;
  10.     }
  11.     int len = atoi(argv[1]);
  12.     int range = atoi(argv[2]);

  13.     int *p = genArray(len,range);
  14.     int *data = (int*)malloc(sizeof(int)*len);

  15.     memcpy(data,p,4*len);
  16.     printforeach("Pop before",data,len);
  17.     do_test(bubble_sort,data,len);
  18.     printforeach("Pop after ",data,len);

  19.     memcpy(data,p,4*len);
  20.     printforeach("select before",data,len);
  21.     do_test(select_sort,data,len);
  22.     printforeach("select after ",data,len);

  23.     memcpy(data,p,4*len);
  24.     printforeach("Insert before",data,len);
  25.     do_test(insert_sort,data,len);
  26.     printforeach("Insert after ",data,len);

  27.     memcpy(data,p,4*len);
  28.     printforeach("Shell before",data,len);
  29.     do_test(shell_sort,data,len);
  30.     printforeach("Shell after ",data,len);

  31.     memcpy(data,p,4*len);
  32.     printforeach("merge before",data,len);
  33.     do_test(merge_sort,data,len);
  34.     printforeach("merge after ",data,len);

  35.     memcpy(data,p,4*len);
  36.     printforeach("heap before",data,len);
  37.     do_test(heap_sort,data,len);
  38.     printforeach("heap after ",data,len);

  39.     memcpy(data,p,4*len);
  40.     printforeach("quick before",data,len);
  41.     do_test2(quick_sort,data,0,len-1);
  42.     printforeach("quick after ",data,len);

  43.     free(p);
  44.     free(data);
  45.     return 0;
  46. }
    Makefile文件的长相如下:

点击(此处)折叠或打开

  1. TARGET = test
  2. SRC = test.c innersort.c
  3. OBJS = $(SRC:.c=.o)
  4. CC = gcc
  5. DEBUG += -pg
  6. INCLUDE = -I.
  7. all:$(TARGET)
  8. $(TARGET):$(OBJS)
  9.     $(CC) $(INCLUDE) $(DEBUG) $(CFLAGS) $(OBJS) -o $(TARGET)
  10. %.o : %.c
  11.     $(CC) $(INCLUDE) $(DEBUG) $(CFLAGS) -c $<
  12. clean:
  13.     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的统计结果,看看快速排序到底是不是真汉子。
   这四篇博文是比较简单的笔记,也仅复习了常见的几种内部排序,外部排序算法还有其他新的算法都没有涉及,有机会再补充。
上一篇:经典排序算法归纳笔记(3)
下一篇:Linux 内核通知链随笔【上】

文章评论