关闭

一些经典排序算法分析

标签: 排序算法分析例子代码
964人阅读 评论(0) 收藏 举报
分类:

前言:这一篇文章中我们将讨论数组排序的问题,对于数据量比较大的,不能在内存中完成排序的,

必须在磁盘上完成排序类型叫作外部排序,本篇将不讨论。

对于内部排序的一些相关知识:

存在几种容易的算法以排序,如插入排序。

有一种算法叫做谢尔排序(ShellSort),它编程非常简单,以运行,并在实践中很有效。

还有一些稍微复杂的的排序算法。

任何通用的排序算法均需要次比较。


1.插入排序

最简单的排序算法之一是插入排序(insertion sort)。这是一个对少量元素进行排序的有效算法,

插入排序基于这样一种假设:位置0到位置p-1上的元素已经是经过排序的,下图显示了一个简单的数组在每一趟排序后的情况。

  

或者可以这样理解,插入排序的机理与打扑克牌时,整理手中牌时的做法一样,在开始摸牌时手中牌是空的,

牌面朝下放在桌子上。接着依次从桌上取一张牌插入到左手牌中正确的位置上,当牌摸完时,左手中的牌就成了有序的状态。

                                                                             


下面给出算法具体代码:

  1. //插入排序    
  2. void InsertSort(int Nums[], int Length)    
  3. {    
  4.     for (int i = 0; i < Length; i++)    
  5.     {    
  6.         int j, temp = Nums[i];//将第i个数保存起来    
  7.         for (j = i - 1; Nums[j] > temp && j >= 0; j--)//依次将第i个数与前面的数比较    
  8.         {    
  9.             Nums[j + 1] = Nums[j];//如果前面的书比第i个数大的,则向后移动    
  10.         }    
  11.         Nums[j + 1] = temp;  //最后将空出的位置装入上面保存的第i个数    
  12.     }    
  13. }    

2.谢尔排序

谢尔排序(Shellsort)的名称源于它的发明者Donald Shell,他通过比较相距一定间隔的元素来工作,

各趟比较所用的距离随着算法的进行二减小,知道只比较相邻元素的最后一趟排序位置。由于这个原因,

谢尔排序有时也叫作缩减增量排序。

                                                            


上图可以看到增量分别为5,3,1的排列状态,5排序之后,从第一个元素开始相隔为5的元素变为有序,

同理3排序和1排序之后元素全部为有序了。

下面给出算法的具体代码

  1. //谢尔排序    
  2. void ShellSort(int Nums[], int Length)  
  3. {  
  4.     for (int gap = Length / 2; gap > 0; gap /= 2)  
  5.     {  
  6.         //根据增量gap进行插入排序  
  7.         for (int i = gap; i < Length; i += gap)  
  8.         {  
  9.             int j, temp = Nums[i];  
  10.             for (j = i; j - gap >= 0; j -= gap)  
  11.             {  
  12.                 if (temp < Nums[j - gap])  
  13.                     Nums[j] = Nums[j - gap];  
  14.                 else  
  15.                     break;  
  16.             }  
  17.             Nums[j] = temp;  
  18.         }  
  19.     }  
  20. }  

上面代码中的gap增量叫做谢尔增量,增量的使用对于算法性能有影响,使用谢尔增量时,
谢尔排序的最坏情形运行时间为,Hibbard提出一个稍微不同的增量序列1,3,7....,
使用Hibbard增量的谢尔排序的最坏情形运行时间为


3.堆排序
对排序的运行时间为,也是一种原地排序算法(任何时候数组中只有常数个元素存储在输入数组以外),
堆的数据结构可以构成一个有效的优先队列,
它的应用可以参考我以前的文章http://blog.csdn.net/itcastcpp/article/details/12999595 。
堆是一棵被填满的二叉树,但底部可以例外,底部的节点从左到右的填充,这样的树被称为完全二叉树(complete binary tree)。
如下图:


完全二叉树有一个很有规律,可以用一个数组表示,而不需要链。对于数组任一位置i上的元素,其左儿子节点在2i上,
右儿子节点在2i+1上,其父节点在2/i上。因此这里不仅不需要链,遍历该树所需要的操作也及简单,
在大部分计算机上运行得非常快,这种实现的唯一问题在于,最大堆的大小需要事先估计,
但一般情况下这不成问题(而且如果需要我们可以重新调整)。
那么对于堆排序我们可以结合上图的树来理解就非常容易了,最后一个元素位置为p,
那么我们将p/2节点和之前分别进行调整,使得1 到 p/2的节点都比其儿子节点大,从p/2开始调整,
递减到第一个根节点,即可以得到根节点最大,再将根节点和最后节点p交换,然后将节点p排除在树之外,
从(p-1)/2的节点开始调整,把最大的根节点跟第p-1个节点交换,那么重复上面过程,直到只剩下根节点,
即完成了一次堆排序,排序结果位从小到大排列。
我们现在对下面实例进行分析:

可以看到上面有10个元素,那么先对第5个元素,发现节点5大于10,不用交换,
进入下一步,第4个节点和他的第8、9两个儿子节点比较:

发现第四个节点比第八个节点小,此时交换节点,然后处理继续处理第3个节点,依次类推,直到第一个节点(根节点)如下图:


最后得到图上图F,可以看到根节点16是最大的。
这时我们将根节点和最后一个节点互换,并且从数种去掉,那么只剩下9个节点了。

我们接着调整根节点在整棵数种的位置,发现根节点比左右子节点都大,不用调整,

这时我们继续将根节点14和最后一个节点1互换,那么1就变成了根节点,再调整1在根节点中的位置:


调整后如上图,根节点为10了,再与最后一个节点互换,然后调整根节点位置:


依此类推,知道所有节点从树中分离:


全部分离后,排列的内容为,可以看到内容变为有序了。


下面给出算法代码:

  1. #include <stdio.h>     
  2.     
  3. //这个函数调整对数组中第n个元素的位置    
  4. void HeapAdjust(int array[], int n, int length)    
  5. {    
  6.     int Child;    
  7.     for (int i = n; i * 2 <= length; i = Child)    
  8.     {    
  9.         Child = i * 2;    
  10.         if (Child + 1 <= length && array[Child] < array[Child + 1])    
  11.             Child++;    
  12.         //如果较大的子节点大于父节点则交换位置    
  13.         if (array[i] < array[Child])    
  14.         {    
  15.             int Temp = array[i];    
  16.             array[i] = array[Child];    
  17.             array[Child] = Temp;    
  18.         }    
  19.         else    
  20.         {    
  21.             break;    
  22.         }    
  23.     }    
  24. }    
  25.     
  26. void HeapSort(int array[], int length)    
  27. {    
  28.     //调整前半部分,保证了最大的值都在前半部分    
  29.     for (int i = length / 2; i > 0; i--)    
  30.     {    
  31.         HeapAdjust(array, i, length);    
  32.     }    
  33.     for (int i = length-1; i > 0; i--)    
  34.     {    
  35.         //将最大的数移动到尾部    
  36.         int Temp = array[1];    
  37.         array[1] = array[i+1];    
  38.         array[i+1] = Temp;    
  39.         //除去尾部后,调整第一个元素位置    
  40.         HeapAdjust(array, 1, i);    
  41.     }    
  42. }    
  43.     
  44. void HeapAdjustLittle(int array[], int num, int length)    
  45. {    
  46.     //如果输入的数小于这些数,直接返回    
  47.     if (num < array[1])    
  48.     {    
  49.         return;    
  50.     }    
  51.     
  52.     //如果输入的数大于数组中最小的数,则赋值,然后调整堆数组    
  53.     array[1] = num;    
  54.     int Child;    
  55.     for (int i = 1; i * 2 <= length; i = Child)    
  56.     {    
  57.         Child = i * 2;    
  58.         if (Child + 1 <= length && array[Child] > array[Child + 1])    
  59.             Child++;    
  60.         //如果较小的子节点大于父节点则交换位置    
  61.         if (array[i] > array[Child])    
  62.         {    
  63.             int Temp = array[i];    
  64.             array[i] = array[Child];    
  65.             array[Child] = Temp;    
  66.         }    
  67.         else    
  68.         {    
  69.             break;    
  70.         }    
  71.     }    
  72. }    
  73.     
  74. //打印出数组内容    
  75. void PrintArray(int array[], int size)    
  76. {    
  77.     printf("最大的前%d个数:\n", size);    
  78.     for (int i = 0; i < size; i++)    
  79.     {    
  80.         printf("%3d", array[i]);    
  81.     }    
  82.     printf("\n");    
  83. }    
  84.     
  85. int myarray[] = { 0, 1, 9, 2, 8, 3, 7, 4, 6, 5 , 10};    
  86.     
  87. int main()    
  88. {    
  89.     //将前十个数进行一次堆排序,并输出结果    
  90.     HeapSort(myarray, sizeof(myarray) / 4 - 1);    
  91.     PrintArray(myarray + 1, sizeof(myarray) / 4 - 1);    
  92.     
  93.     //输入数字,打印出前十个最大的数    
  94.     while (1)    
  95.     {    
  96.         int num = 0;    
  97.         scanf("%d", &num);    
  98.         HeapAdjustLittle(myarray, num, sizeof(myarray) / 4 - 1);    
  99.         PrintArray(myarray + 1, sizeof(myarray) / 4 - 1);    
  100.     }    
  101.     
  102.     return 0;    
  103. }    

4.归并排序

归并排序以最坏情形的运行时间运行,而所使用的比较次数几乎是最优的。

它是递归算法的一个很好的实例。

这个算法中的基本操作是合并两个已排序的表。因为这两个表是已排序的,所以若将输出放到第三个表中,则该算法

可以通过对输入数据进行一趟排序来完成。

比如有两个数组    1 3 5 7   和 2 4 6 8将他们合并到其三个数组中,第一次1和2相比较,1比较小,则将1放到第三个数组中

1 3 5 7     2 4 8 10          1

再将将3和2比较得到

1 3 5 7     2 4 8 10          1 2

3和4开始比较得到

1 3 5 7     2 4 8 10          1 2 3

5和4比较得到

1 3 5 7     2 4 8 10          1 2 3 4

5和8比较得到

1 3 5 7     2 8 10          1 2 3 4 5

7和8比较得到

1 3 5 7     2 8 10          1 2 3 4 5 7

第一个数组使用完了,我们将第二个数组剩余的 8和10拷贝到第三个数组尾部得到

1 2 3 4 5 7 8 10

这样一次合并就完成了。

可以看到合并的时间显然是线性的,因为最多进行了N-1次比较,其中N是元素总数。

我们可以看到每一次比较总会将小的数放大第三个数组中,但是最后一次比较至少可以直接添加两个元素到

第三个数组中。

因此归并算法很容易描述,如果N=1,那么只有一个元素需要排序,我们排序直接完成,然后直接将前半部分和

后半部分进行合并,得到元素为2的有序数组,再对元素为2的相邻数组进行合并,如此递归进行,得到最后的有序数组。


下面直接看代码对照理解:

  1. #include <stdio.h>  
  2.   
  3. /** 
  4. *合并两个有序数组 
  5. */  
  6. void Merge(int Array[], int tmpArray[], int left, int mid, int right)  
  7. {  
  8.     int i = left;  
  9.     int tmpleft = left;  
  10.     int tmpright = mid+1;  
  11.     //比较左右数组元素大小,放入第三个数组中  
  12.     while (tmpleft <= mid && tmpright <= right)  
  13.     {  
  14.         if (Array[tmpleft] < Array[tmpright])  
  15.             tmpArray[i++] = Array[tmpleft++];  
  16.         else  
  17.             tmpArray[i++] = Array[tmpright++];  
  18.     }  
  19.     //右边数组用完了进入此循环拷贝左边数组到第三个数组  
  20.     for (; tmpleft <= mid; tmpleft++)  
  21.     {  
  22.         tmpArray[i++] = Array[tmpleft];  
  23.     }  
  24.     //左边数组用完了进入此循环拷贝右边数组到第三个数组  
  25.     for (; tmpright <= right; tmpright++)  
  26.     {  
  27.         tmpArray[i++] = Array[tmpright];  
  28.     }  
  29.     //合并后的数组拷贝回原数组  
  30.     for (i = left; i <= right; i++)  
  31.     {  
  32.         Array[i] = tmpArray[i];  
  33.     }  
  34. }  
  35.   
  36. /** 
  37.   *归并排序 
  38.   */  
  39. void MergeSort(int Array[], int tmpArray[], int left, int right)  
  40. {  
  41.     //基准条件,如果只有一个元素即为有序数组,直接返回  
  42.     if (left >= right)  
  43.         return;  
  44.     //递归数组排序左右两边数组  
  45.     int mid = (left + right) / 2;  
  46.     MergeSort(Array, tmpArray, left, mid);  
  47.     MergeSort(Array, tmpArray, mid + 1, right);  
  48.     //排序完成后进入和并数组  
  49.     Merge(Array, tmpArray, left, mid, right);  
  50. }  
  51.   
  52. int main()  
  53. {  
  54.     int a[] = { 1, 0, 2, 9, 3, 8, 4, 7, 5, 6 };  
  55.     int tempa[10];  
  56.     MergeSort(a, tempa, 0, 9);  
  57.     for (int i = 0; i < 10; i++)  
  58.     {  
  59.         printf("%d \n", tempa[i]);  
  60.     }  
  61.     return 0;  
  62. }  

虽然归并排序的运行时间是,但是它很难用于主存排序,主要问题在于合并两个排序的表需要线性附加内存,

在整个算法中还要花费将数据复制到临时数组再复制回来这样的一些附加工作,其结果是严重减慢了排序的速度。这种复制可以

通过在递归的交替层面上审慎的交换Array和tmpArray的角色加以避免。归并排序的一种变形也可以非递归的实现。

与其他的排序相比,归并排序的运行时间很大程度上依赖于在数组中进行元素的比较和移动所消耗的时间。

这些消耗是和编程语言相关的。

例如,在其他语言(例如Java)中,当排序一般的对象时,元素的比较耗时很多,但是移动元素就快得多。在所有流行的排序算法中,归并排序使用最少次数比较。因此,在Java中,归并排序是一般目的的排序的最佳选择。事实上,在标致Java库中的一般排序就是用的这种算法。

另一方面,在C++中,对于一般排序,当对象很大时,赋值对象的代价是很大的,二对象比较通常相对消耗小些。这是因为编译器在处理函数模板的扩展时具有强大的执行在线优化的能力。在本节中,如果我们可以使用很少的数据移动,那么即使使用稍微多一些比较的算法也是合理的。下面介绍的快速排序算法较好的平衡了这两者,而且也是C++库中普遍使用的排序历程。


5.快速排序

顾名思义,快速排序(quicksort)是在实践中最快的一直排序算法,它的平均运行时间是。改算法之所以特别快,主要是由于非常精炼和高度优化的内部循环。它的最坏情形的性能为,但稍加努力就可避免这种情形。通过将堆排序与快速排序结合起来,就可以在堆排序的最坏运行时间下,得到对几乎所有输入的最快运行时间。

虽然多年来快速排序算法曾被认为是理论上高度优化而在实践中不可能正确编程的一种算法,但是该算法简单易懂而且不难证明。想归并排序一样,快速排序也是一种分治的递归算法。

下面通过实例分析快速排序:

在待排序的数组中:



随机的选择一个枢纽元(pivot) 65:



然后进行划分,将比枢纽元65小的放到左边,其他的放到右边:



那么一次快速排序完成,然后递归的对左边数组和右边数组分别进行快速排序。

最后完成后数组即成为有序:



下面我们给出一种快速排序的算法代码:

  1. #include <stdio.h>  
  2.   
  3. //分为两个区,左边的数都比右边的数小,    
  4. //返回值为中间数所在的位置    
  5. int Partition(int Nums[], int left, int right)  
  6. {  
  7.     int midNum = Nums[right];//定数组最后一个数位中间数    
  8.     int j = left;  
  9.     for (int i = left; i < right; i++)//循环比较第i个数和中间数    
  10.     {  
  11.         if (Nums[i] < midNum)//如果小于中间数的,j指针就向后移动,    
  12.             //j指针之前的数都小于中间数    
  13.         {  
  14.             if (i != j)  
  15.             {  
  16.                 int temp = Nums[i];  
  17.                 Nums[i] = Nums[j];  
  18.                 Nums[j] = temp;  
  19.             }  
  20.             j++;  
  21.         }  
  22.     }  
  23.     //收尾工作,将中间数和j指向的中间位置的数相调换    
  24.     int temp = Nums[j];  
  25.     Nums[j] = midNum;  
  26.     Nums[right] = temp;  
  27.   
  28.     return j;//返回中间数的下标    
  29. }  
  30.   
  31. void QuickSort(int Nums[], int left, int right)  
  32. {  
  33.     if (left < right)  
  34.     {  
  35.         int mid = Partition(Nums, left, right);//分区    
  36.         QuickSort(Nums, left, mid - 1);//左部分递归排序    
  37.         QuickSort(Nums, mid + 1, right);//右部分递归排序    
  38.     }  
  39. }  
  40.   
  41. int main()  
  42. {  
  43.     int a[] = { 1, 0, 2, 9, 3, 8, 4, 7, 5, 6 };  
  44.     QuickSort(a, 0, 9);  
  45.     for (int i = 0; i < 10; i++)  
  46.     {  
  47.         printf("%d \n", a[i]);  
  48.     }  
  49.     return 0;  
  50. }  

任何只使用比较的一般排序算法在最坏情形下需要时间,但是在某些特殊情况下以线性时间进行

排序仍然是可能的,下一篇我们将介绍不是基于比较的排序。


接下来我们来讲两种非比较排序,计数排序,基数排序

计数排序

计数排序是一种非比较算法,其时间复杂度为O(N+K)

 

举例说明

先用一个例子来说明计数排序算法,比如需要排序的集合为{1, 2, 1, 0, 4, 5},在该集合中,最大的数值为5,那么通过遍历整个集合,

可以得到这样的数组

   int counter[] = {1, 2, 1, 0, 1, 1}

                              0, 1, 2, 3, 4, 5

counter数组描述了被排序数组里有1个0, 2个1, 1个2, 0个3, 1个4和1个5,当这个数组形成时,排序也就结束了。

 

代码设计

1)根据集合中最大的数值K,来申请辅助空间counter数组

2)遍历被排序集合,讲计数记录到counter数组中

 

代码实现

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4.   
  5. int data[] = {1, 2, 1, 0, 4, 5};  
  6.   
  7. // 计数排序参数列表  
  8. // int d[] 为待排序数据列表  
  9. // int n 为待排序数据个数  
  10. // int min, max为待排序数据中最大和最小的数,通过其计算待排数据跨度K  
  11. void sort_counter(int d[], int n, int k)  
  12. {  
  13.     int i, j = 0;  
  14.     k++; // 实际申请空间比K大1  
  15.     // 申请辅助空间  
  16.     int* counter = malloc(sizeof(int)*k);  
  17.     memset(counter, 0, sizeof(int)*k);  
  18.   
  19.     // 计数  
  20.     for(i=0; i<n; ++i)  
  21.     {  
  22.         ++counter[d[i]];  
  23.     }  
  24.   
  25.     // 讲计数结果保存到待排数据空间  
  26.     for(i=0; i<k; ++i)  
  27.     {  
  28.         while(counter[i]-- > 0)  
  29.         {  
  30.             d[j++] = i;  
  31.         }  
  32.     }  
  33.   
  34.     // 释放辅助空间  
  35.     free(counter);  
  36. }  
  37.   
  38. int main(void)  
  39. {  
  40.     sort_counter(data, 6, 5);  
  41.     int i;  
  42.     for(i=0; i<6; ++i)  
  43.     {  
  44.         printf("%d\n", data[i]);  
  45.     }  
  46.     return 0;  
  47. }  




特点

计数排序的特单是时间复杂度,与其他的排序算法不同,它的时间复杂度为O(N+K),这个时间复杂度表明了当K相对比较大时,不适合使用,比如对集合{1, 0, 100}

但是对于N远大于K的情况下,是相当适合的

 

基数排序

在数量大时,计数排序需要大量的辅助空间,并且时间复杂度有可能比较大,所以推出基数排序。

举例说明

假如有待排序数据 data[] = {312, 213, 545, 893};

先排序个位数:排序结果为 312, 213, 893, 545

再排序十位数:排序结果为 321, 213, 545, 893

再排序百位数:排序结果为 213, 312, 545, 893

完毕

对位数的排序,可以使用计数排序,速度快而且稳定。

 

代码设计

1)获取待排序数据中的最大位数

2)对位数进行循环,按位对待排序数据进行计数排序,并将其中间结果保存到临时空间

3)将临时数据保存到待排序数据,继续步骤2)

 

代码实现

  1. #include <stdio.h>  
  2.   
  3. // int data[] = {1, 100, 321, 121, 333, 586, 1100};  
  4.   
  5. // 该函数计算data中最大位数,本例子中,最大位数是1100,所以结果是4  
  6. int maxbit(int data[],int n)  
  7. {  
  8.     int d = 1; //保存最大的位数  
  9.     int p =10;  
  10.     for(int i = 0;i < n; ++i)  
  11.     {  
  12.         while(data[i] >= p)  
  13.         {  
  14.             p *= 10;  
  15.             ++d;  
  16.         }  
  17.     }  
  18.     return d;  
  19. }  
  20.   
  21. // 基数排序  
  22. void sort_radix(int data[],int n)  
  23. {  
  24.     int d = maxbit(data, n);    // 计算位数  
  25.     int * tmp = new int[n];     // 中间变量,用来存储中间排序结果  
  26.     int * count = new int[10];  // 计数排序中的计数器  
  27.     int i,j,k;  
  28.     int radix = 1;  
  29.   
  30.     // 根据最大位数进行循环,对没一位进行计数排序  
  31.     for(i = 1; i<= d;i++)  
  32.     {  
  33.         // 初始化计数器  
  34.         for(j = 0; j < 10; j++)  
  35.             count[j] = 0;  
  36.   
  37.         // 对位数进行计数排序  
  38.         for(j = 0; j < n; j++)  
  39.         {  
  40.             k = (data[j]/radix)%10; // 注意这里进行取模  
  41.             count[k]++;             // 计数  
  42.         }  
  43.         for(j = 1; j < 10; j++)  
  44.             count[j] = count[j-1] + count[j];  
  45.   
  46.         // 将排序中间结果保存到tmp  
  47.         for(j = n-1; j >= 0;j--)  
  48.         {  
  49.             k = (data[j]/radix)%10;  
  50.             tmp[count[k]-1] = data[j];  
  51.             count[k]--;  
  52.         }  
  53.   
  54.         // 将中间结果保存到data  
  55.         for(j = 0;j < n;j++)  
  56.             data[j] = tmp[j];  
  57.   
  58.         // 取模时的被除数,需要提高一位  
  59.         radix = radix*10;  
  60.     }  
  61.     delete [] tmp;  
  62.     delete [] count;  
  63. }  
  64.   
  65. int main()  
  66. {  
  67.     int data[] = {1, 100, 321, 121, 333, 586, 1100};  
  68.     sort_radix(data, 7);  
  69.     for(int i=0; i<7; i++)  
  70.     {  
  71.         printf("%d\n", data[i]);  
  72.     }  
  73.     return 0;  
  74. }  


特点

基数排序比较适合对取值很大的数进行排序,也可用来对字符串进行排序。

但基数排序的缺点是不呈现时空局部性,因为在按位对每个数进行排序的过程中,一个数的位置可能发生巨大的变化,所以不能充分利用现代机器缓存提供的优势。同时计数排序作为中间稳定排序的话,不具有原地排序的特点,当内存容量比较宝贵的时候,还是有待商榷。



本文中缺少一些图,具体可参考:
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:575702次
    • 积分:6517
    • 等级:
    • 排名:第3691名
    • 原创:23篇
    • 转载:295篇
    • 译文:0篇
    • 评论:30条
    技术连接
    1、stackoverflow:http://stackoverflow.com/ 2、何登成的技术博客:http://hedengcheng.com/?p=828