现在的位置: 首页 > 综合 > 正文

经典排序算法设计与分析(插入排序、冒泡排序、选择排序、shell排序、快速排序、堆排序、分配排序、基数排序、桶排序、归并排序)

2019年05月16日 ⁄ 综合 ⁄ 共 9630字 ⁄ 字号 评论关闭

本文章转自高手http://blog.csdn.net/wanghao109/article/details/15506253


测试用例:

为了分析所有的排序情况,给出一个模板程序用于测试,通过改写mySort函数来实现不同的排序算法。测试环境为vc++6.0。可以通过改变SIZE的大小来减少或增长排序所需的时间。另外测试时间还包括函数调用时间,存在一定的误差。

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <time.h>  
  4.   
  5. //元素的数目  
  6. #define SIZE 10000  
  7.   
  8. //打印数组中的所有元素  
  9. void printAll(int buff[],int len = SIZE)  
  10. {  
  11.     int i;  
  12.     for (i = 0;i < len;i++)  
  13.     {  
  14.         printf("%d\n",buff[i]);  
  15.     }  
  16. }  
  17.   
  18. //排序算法  
  19. void mySort(int buff[],int len = SIZE)  
  20. {  
  21. }  
  22.   
  23. int main()  
  24. {  
  25.     int i;  
  26.     int buff[SIZE];  
  27.     double t1,t2;  
  28.       
  29.     srand(time(NULL));//生成随机数种子  
  30.     for (i=0;i<SIZE;i++)  
  31.     {  
  32.         buff[i] = rand()/SIZE;//随机生成元素  
  33.     }  
  34.   
  35.     t1=(double)clock()/CLK_TCK;  
  36.     mySort(buff);//调用排序算法  
  37.     t2=(double)clock()/CLK_TCK;  
  38.     printf("Total time used:%f second\n",t2-t1);//计算排序所用的事件  
  39.   
  40.     printAll(buff,20);//打印前面的元素,用于验证  
  41.   
  42.     return 0;  
  43. }  

1.插入排序(Insert Sort)

用算法导论中的一个例子来形容,插入排序就好像你打扑克摸牌。第一次摸一张10(不用排序);第二次摸一张5,你就需要把5插入到10后面;第三次摸了一张7,你就需要将7插到5的后面、10的前面,以此类推。插入排序的时间复杂度n2。

  1. void mySort(int buff[],int len = SIZE)  
  2. {  
  3.     int i,j,key;  
  4.     for (i = 1;i < len;i++)//i表示你摸了的第i张牌,第一张牌不用排序,可以跳过  
  5.     {  
  6.         key = buff[i];//先记住这张牌的值  
  7.         for (j = i;j > 0 && buff[j-1] > key;j--)//将这张牌依次与它之前的每张牌进行比较,直到找到比它小的牌为止  
  8.         {  
  9.             buff[j] = buff[j-1];//在查找的过程中,将对比过的牌向后移。  
  10.         }  
  11.         buff[j] = key;//将牌插入到指定位置  
  12.     }  
  13. }  

运行结果:

2.冒泡排序(Bubble Sort)

假设有10个数,先比较第10和第9个数,将较小的数存到9的位置;然后比较第9和8个数,此时得到的较小的数也就是第8,9,10中的最小的数。依次类推,当比较完第1和2个数之后,第一个位置存放的数就是所有元素中最小的数。在从第10个开始比较,最终将第2小的数放到第二个位置。依次执行就可以完成排序了。冒泡排序的时间复杂度n2。

  1. void mySort(int buff[],int len = SIZE)  
  2. {  
  3.     int i,j,t;  
  4.     for(i = 0;i < len-1;i++)//最后只剩下一个元素时不需要比较  
  5.     {  
  6.         for (j = len-1;j > i;j--)////从最后一个元素开始冒泡,只需要比较到i就可以了(因为i之前的元素都已经排好序了)  
  7.         {  
  8.             if (buff[j] < buff[j-1])//依次比较相邻的两个元素  
  9.             {  
  10.                 t = buff[j];  
  11.                 buff[j] = buff[j-1];  
  12.                 buff[j-1] =t;  
  13.             }  
  14.         }  
  15.     }  
  16. }  

运行结果:

3.选择排序

选择排序和冒泡排序的方法基本类似,只不过它减少了交换了次数。每次比较时都只记录下最小值得下标,不进行时间的交换,直到最后才将得到的下标和第一个元素进行唯一的依次交换。选择排序的时间复杂度n2。

  1. void mySort(int buff[],int len = SIZE)  
  2. {  
  3.     int i,j,t;  
  4.     for(i = 0;i < len-1;i++)//最后只剩下一个元素时不需要比较  
  5.     {  
  6.         int index = i;//记录最小值的下标  
  7.         for (j = len-1;j > i;j--)//从最后一个元素开始冒泡,只需要比较到i就可以了(因为i之前的元素都已经排好序了)  
  8.         {  
  9.             if (buff[j] < buff[index])//比较当前元素和当前的最小值  
  10.             {  
  11.                 index = j;  
  12.             }  
  13.         }  
  14.         //将最小值放到最前面的位置  
  15.         t = buff[index];  
  16.         buff[index] = buff[i];  
  17.         buff[i] = t;  
  18.     }  
  19. }  

运行结果:

4.shell排序

希尔排序也称为缩小增量排序(diminishing increment sort),它利用了插入排序最佳时间代价特性(即元素的初始排列就接近于有序,那么插入排序比较的次数最少,循环时间最短)。通过逐渐缩小增量,来将数组尽量的变得有序,再使用插入排序,这样效率更高。选择适量的增量可以提高shell排序的效率。sell排序的时间复杂度n1.5(增量每次除以3)。

  1. void insertSort(int buff[],int len = SIZE,int increase = 1)  
  2. {  
  3.     int i,j,key;  
  4.     for (i = increase;i < len;i+=increase)//以increase为标准取元素  
  5.     {  
  6.         key = buff[i];  
  7.         for (j = i;j >= increase && buff[j-increase] > key;j -= increase)//以increase为元素之间的间隔距离  
  8.         {  
  9.             buff[j] = buff[j-increase];  
  10.         }  
  11.         buff[j] = key;  
  12.     }  
  13. }  
  14.   
  15. //排序算法  
  16. void mySort(int buff[],int len = SIZE)  
  17. {  
  18.     int setp = 2;//每次缩小的倍数,不能为1,为1表示不缩小  
  19.     int i,j;  
  20.     for(i = len/setp;i >= 1;i /= setp)  
  21.     {  
  22.         for (j=0;j<i;j++)  
  23.         {  
  24.             insertSort(buff,len-j,i);  
  25.         }  
  26.     }  
  27.   
  28. }  

运行结果:

这是shell排序以2为增量的示例图:



5.快速排序

考虑一个BST(二叉搜索树)通过中序遍历得到的有序数组,我们可以知道BST的根节点将树分为两个部分。左子树的所有记录都比根节点要小,右子树的所有记录都比根节点要大。这里就是一种“分治”的思想。同理,将数组根据key来分为两部分,一部分比key要小,一部分比key要大。对每一部分继续划分,直到每个子部分都只包含一个元素,不就是得到了类似BST的数组了吗。快速排序的时间复杂度n*lgn。

  1. //交换数组中地址为index1,index2两元素的值  
  2. void swap(int buff[],int index1,int index2)  
  3. {  
  4.     int t = buff[index1];  
  5.     buff[index1] = buff[index2];  
  6.     buff[index2] = t;  
  7. }  
  8.   
  9. //将大数组划分为小数组,使得l左边都是小于keyValue的值,r右边都是大于keyValue的值  
  10. int partition(int buff[],int l,int r,int keyValue)  
  11. {  
  12.     do   
  13.     {  
  14.         while (buff[++l] < keyValue);//找到从左起,第一个大于keyValue的数,用于交换  
  15.   
  16.         while ( r!=0 && buff[--r] > keyValue);//找到从右起,第一个小于keyValue的数,用于交换  
  17.         /* 
  18.         注意之所以要考虑r!=0,是为了应对特殊情况,即所有元素都大于keyValue(即数组本来就有序), 
  19.         此时l位于到数组最左边,r一直移动,知道数组的最左边仍然满足buff[--r]>keyValue,继续运动就会越界,所以需要r!=0 
  20.         */  
  21.   
  22.         swap(buff,l,r);//将一个大值和一个小值交换  
  23.     } while (l<r);  
  24.   
  25.     swap(buff,l,r);//由于使用的是do,while,所以在判断l<r之前进行过依次交换,但这次交换是多余的,需要再换回来  
  26.   
  27.     return l;//返回分割后右边部分的头位置  
  28. }  
  29.   
  30.   
  31. //排序算法,第一次调用使用mySort(buff,0,SIZE-1)  
  32. void mySort(int buff[],int begin,int end)  
  33. {  
  34.     if (end <= begin)//0个或1个元素,此时排序已经完成,不在进行  
  35.     {  
  36.         return;  
  37.     }  
  38.   
  39.     int keyIndex = (begin+end)/2;//使用中间值作为比较用的key  
  40.       
  41.     swap(buff,keyIndex,end);//将key放到最右边,便于partition中使用  
  42.   
  43.     int k = partition(buff,begin-1,end,buff[end]);  
  44.   
  45.     swap(buff,k,end);//总是将key放到两个部分的中间  
  46.   
  47.     mySort(buff,begin,k-1);  
  48.     mySort(buff,k+1,end);  
  49.   
  50. }  

运行结果:

这里还有另一个版本的快速排序,只不过是以每个部分数组的第一个元素为轴值。

  1. int partition(int list[],int low,int high)  
  2. {  
  3.     int tmp = list[low];  
  4.     while (low < high) {    
  5.             while (low < high && list[high] >= tmp) {    
  6.                 high--;    
  7.             }    
  8.             list[low] = list[high];   //比中轴小的记录移到低端    
  9.             while (low < high && list[low] <= tmp) {    
  10.                 low++;    
  11.             }    
  12.             list[high] = list[low];   //比中轴大的记录移到高端    
  13.         }    
  14.     list[low] = tmp;              //中轴记录到尾    
  15.     return low;    
  16. }  
  17.   
  18.   
  19. //排序算法,第一次调用使用mySort(buff,0,SIZE-1)  
  20. void mySort(int list[],int low,int high)  
  21. {  
  22.     if (low < high) {    
  23.             int middle = partition(list, low, high);  //将list数组进行一分为二    
  24.             mySort(list, low, middle - 1);        //对低字表进行递归排序    
  25.             mySort(list, middle + 1, high);       //对高字表进行递归排序    
  26.         }    
  27.   
  28. }  

6.堆排序

堆是一种树形结构,最大堆中所有父节点的值都大于其子节点,最小堆相反。堆排序实质就是不断从最小堆中取出根节点,然后重新调整堆的结构,使它满足堆的性质。堆排序的时间复杂度n*lgn。

  1. void swap(int *a,int *b)  
  2. {  
  3.     int t = *a;  
  4.     *a = *b;  
  5.     *b = t;  
  6. }  
  7.   
  8.   
  9. /***  
  10.  * a            待排数组  
  11.  * rootIndex    本次堆化的根  
  12.  * maxHeapIndex 本次堆化所达到的堆数组最大索引  
  13.  */    
  14. void maxHeapify2(int a[], int rootIndex,int maxHeapIndex)    
  15. {    
  16.     int lChild = rootIndex*2+1; //左儿子节点   
  17.     int rChild = rootIndex*2+2;  //右儿子节点  
  18.   
  19.     //得到父节点,左右儿子节点中最大的节点,并与父节点交换  
  20.     int largest = rootIndex;    
  21.     if(lChild <= maxHeapIndex && a[lChild] > a[rootIndex])    
  22.         largest = lChild;    
  23.     if(rChild <= maxHeapIndex && a[rChild] > a[largest])    
  24.         largest = rChild;    
  25.   
  26.     if(largest != rootIndex)    
  27.     {    
  28.         swap(&a[largest],&a[rootIndex]);    
  29.         maxHeapify2(a,largest,maxHeapIndex);    
  30.     }    
  31. }   
  32.   
  33. void heapSort2(int a[],int len)   
  34. {    
  35.     int i=0;    
  36.     int maxHeapIndex = len-1;    
  37.     //首先建堆    
  38.     for(i = (maxHeapIndex-1)/2; i >= 0;i--)    
  39.     {    
  40.         maxHeapify2(a,i,maxHeapIndex);    
  41.     }    
  42.     //最大的值放到数组末尾,然后对剩下的数组部分进行堆排序  
  43.     for(i = maxHeapIndex;i >= 1;i--)    
  44.     {    
  45.         swap(&a[0],&a[i]);    
  46.         // 交换之后,继续堆化时,堆数组最大索引要减1    
  47.         maxHeapify2(a,0,i-1);    
  48.     }    
  49. }    
  50.   
  51.   
  52. //排序算法  
  53. void mySort(int buff[],int len = SIZE)  
  54. {  
  55.     heapSort2(buff,len);  
  56. }  

运行结果:可以看出堆排序对大数组速度很快的



7.分配排序

示例代码:

  1. for(i = 0;i<n;i++)  
  2. {  
  3.     B[ A[i] ] = A[i];  
  4. }  

时间复杂度n,但不管是存储空间,还是查询效率都极低(每次必须要遍历所有的位置)。


8.基数排序

示例代码:

  1. /* 
  2. 假设n个d位的元素存放在数组A中,其中1为最低位,第d位为最高位 
  3. RADIX-SORT(A,d) 
  4.     for i = 0  to d 
  5.         use a stable sort to sort array A on  digit i 
  6. */  

算法示意图:


基数排序时间复杂度为O (nlog(r)m),其中r为所采取的基数,m为堆数

9.桶排序

桶排序假设输入数据服从均匀分布,平均情况下时间代价为n。与基数排序类似。根据n个输入数据放到[ 0 ,1 )划分的n个相同大小的子区间,这些子区间称为桶。我们先对桶中的数据进行排序,然后遍历每个桶,得到结果。

伪代码:

  1. /* 
  2. 假设输入是一个包含n个元素的数组A,且每个元素A[i]满足0<=A[i]<1. 
  3. BUCKRT-SORT(A) 
  4.     n = A.length 
  5.     let B[0,··,n-1] be a new array 
  6.     for i = 0 to n-1 
  7.         make B[i] an empty list 
  8.     for i = 1 to n 
  9.         insert A[i] into list B[ nA[i]下限 ] 
  10.     for i = 0 to n-1 
  11.         sort list B[i] with insertion sort 
  12.  
  13.   concatenate the lists B[0],B[1],···,B[n-1] together in order 
  14. */  

算法示意图:


10.归并排序

归并排序将一个序列分成两个长度相等的子序列,为每一个子序列排序,然后再将它们合并成一个序列。时间复杂度n*lgn。

  1. void mergeSort(int a[],int temp[],int left,int right)  
  2. {  
  3.     /* 
  4.     如果数组很小,直接使用插入排序 
  5.     */  
  6.   
  7.     int mid = (left+right)/2;  
  8.     if (left == right)  
  9.     {  
  10.         return;  
  11.     }  
  12.   
  13.     //将数组分成两个子序列  
  14.     mergeSort(a,temp,left,mid);  
  15.     mergeSort(a,temp,mid+1,right);  
  16.   
  17.     int i,j,k;  
  18.   
  19.     //将左序列顺序复制  
  20.     for (i = mid;i >= left;i--)  
  21.     {  
  22.         temp[i] = a[i];  
  23.     }  
  24.   
  25.     //将右序列逆序复制  
  26.     for (j = 1;j <= right-mid;j++)  
  27.     {  
  28.         temp[right-j+1] = a[j+mid];  
  29.     }  
  30.   
  31.     //归并  
  32.     for (i = left,j = right,k = left;k <= right;k++)  
  33.     {  
  34.         if (temp[i]<temp[j])  
  35.             a[k] = temp[i++];  
  36.         else  
  37.             a[k] = temp[j--];  
  38.           
  39.     }  
  40.   
  41. }  
  42.   
  43.   
  44. //排序算法  
  45. void mySort(int buff[],int len = SIZE)  
  46. {  
  47.     int temp[SIZE];  
  48.     mergeSort(buff,temp,0,len-1);  
  49. }  

运行结果:




总结:

1. 运行时间比较

SIZE = 10000
排序算法 平均运行时间(s)  
插入排序        0.143000  
冒泡排序 0.463000  
选择排序 0.221000  
shell排序 0.123000  
快速排序 0.003000  
堆排序 0.008000  
分配排序    
基数排序    
桶排序    
归并排序 0.003000

2、选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法, 冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。

排序法 平均时间 最差情形 稳定度 额外空间 备注
冒泡 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大时较好

3.References:

1.算法导论,第三版

2.数据结构与算法分析(C++),第二版

3.百度及其相关的互联网

4.快速排序:http://blog.csdn.net/z69183787/article/details/8771727

5.堆排序:http://blog.csdn.net/taizhoufox/article/details/5938616

7.各种排序算法分析及比较:http://blog.chinaunix.net/uid-26565142-id-3126683.html

抱歉!评论已关闭.