Chinaunix首页 | 论坛 | 博客
  • 博客访问: 147297
  • 博文数量: 14
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 145
  • 用 户 组: 普通用户
  • 注册时间: 2014-02-12 15:27
个人简介

文章分类

全部博文(14)

文章存档

2014年(14)

分类: C/C++

2014-04-11 12:03:24

    前面的文章里面介绍了几种常用排序算法,通过算法实现可以看出,都是基于比较的方式进行排序,称为比较排序;而且它们的时间下界是O(n lgn)。下面的描述中将介绍三种线性时间排序算法:计数排序、基数排序、桶排序。时间下界O(n lgn)对于这三种排序算法是不适用的,这三种排序算法的下界是O(n)。
一、计数排序
   原理:计数排序的基本思想是:对于每一个元素x,确定了小于等于x的元素的个数,就可以将x放到相应的位置上。主要过程是:
      假设k是待排序数组的最大值。
      1、使用辅助数组c(大小是k),记录[0...k]在数组a中出现的次数。
      2、依次计算数组c,使c[i]表示数组a中不大于i的元素的个数。
      3、从后往前遍历数组a,根据数组c中的个数将a的元素放到辅助数组b(大小等于a)中。
      4、把有序数组b复制到a中。
   代码实现:

点击(此处)折叠或打开

  1. /************************************************************************/
  2. /* 计数排序
  3.     a 输入待排序数组,保存排序后数组
  4.     size 待排序数组长度
  5.     k 待排序数组元素最大值
  6. */
  7. /************************************************************************/
  8. void CountingSort(int *a, int size, int k)
  9. {
  10.     int i = 0;
  11.     int *b = NULL; // 排序结果
  12.     int *c = NULL; // 待排序数组中 0-k的计数和

  13.     c = (int *)malloc(sizeof(int) * (k + 1));
  14.     b = (int *)malloc(sizeof(int) * size);
  15.     memset(c, 0, sizeof(int) * (k + 1));
  16.     memset(b, 0, sizeof(int) * size);

  17.     // 计算排序数组中等于c[i]的个数
  18.     for(i = 0; i < size; i++)
  19.         (*(c + a[i]))++;

  20.     // 计算排序数组中小于等于c[i]的个数
  21.     for(i = 1; i <= k; i++)
  22.         c[i] += c[i - 1];

  23.     // 从后往前遍历a数组,将a数组元素放到b数组的相应位置
  24.     for(i = size - 1; i >= 0; i--)
  25.     {
  26.         b[*(c + a[i]) - 1] = a[i];
  27.         (*(c + a[i]))--;
  28.     }

  29.     // 将b数组赋给a数组
  30.     for(i = 0; i < size; i++)
  31.         a[i] = b[i];

  32.     free(c);
  33.     free(b);
  34. }
    性能分析:从代码可以看出,算法的总时间T = 第一个循环n + 第二个循环k + 第三个循环n + 第四个循环n = 3n + k,时间复杂度是O(n + k)。所以计数排序的时间下界是O(n),在最坏情况下也是不变的。此外计数排序需要n+k的辅助空间,这个辅助空间是比较大的。计数排序是稳定的排序,它不会改变相同元素的顺序,这就是为什么从后往前遍历a数组的原因;而且计数排序是基数排序的基础,这种稳定性也是基数排序需要的。

、基数排序
    原理:
在计数排序中,当k值很大时如({12,1134,99999}),这时需要的辅助空间和时间都会很大,其效率并不比比较排序高。所以基数排序弥补了计数排序的缺陷,基数排序的基本原理是:将数组元素分解成个位(第一位)、十位(第二位)。。。。。。,然后从第一位开始进行计数排序,直到最高位结束,排序完成。由于分解出来的每位数据的最大值是9,因此计数排序的k值为9。
   代码实现:

点击(此处)折叠或打开

  1. /************************************************************************/
  2. /* 获取整数 第loc位的值 */
  3. /************************************************************************/
  4. int digit(int num, int loc)
  5. {
  6.     return (num / (int)pow(10, loc - 1)) % 10;
  7. }
  8. /************************************************************************/
  9. /* 基数排序中的计数排序
  10.     a 排序数组
  11.     n 数组大小
  12.     k 记录最大值
  13.     d 按数组元素的第 d 位排序
  14. */
  15. /************************************************************************/
  16. void CountSort(int *a, int n, int k, int d)
  17. {
  18.     int i = 0;
  19.     int *c = NULL;
  20.     int *b = NULL;

  21.     c = (int *)malloc(sizeof(int) * (k + 1));
  22.     b = (int *)malloc(sizeof(int) * n);
  23.     memset(c, 0, sizeof(int) * (k + 1));
  24.     memset(b, 0, sizeof(int) * n);

  25.     // 计算a[i]中第d位记录的个数
  26.     for(i = 0; i < n; i++)
  27.         (*(c + digit(a[i], d)))++;

  28.     // 计算小于等于a[i]中第d为的记录个数
  29.     for(i = 1; i <= k; i++)
  30.         c[i] += c[i - 1];

  31.     // 从后往前遍历a数组,将元素放到b的相应位置
  32.     for(i = n - 1; i >= 0; i--)
  33.     {
  34.         *(b + *(c + digit(a[i], d)) - 1)= a[i];
  35.         (*(c + digit(a[i], d)))--;
  36.     }

  37.     // 将b复制到a
  38.     for(i = 0; i < n; i++)
  39.         a[i] = b[i];

  40.     free(c);
  41.     free(b);
  42. }
  43. /************************************************************************/
  44. /* 基数排序
  45.     a 待排序数组
  46.     size 数组大小
  47.     d 数组元素的位数
  48. */
  49. /************************************************************************/
  50. void RadixSort(int *a, int size, int d)
  51. {
  52.     int i;
  53.     for(i = 1; i <= d; i++)
  54.     {
  55.         CountSort(a, size, 9, i);
  56.     }
  57. }
    性能分析:基数排序时间T=d*(3n + k),其中d是记录值的位数,(3n + k)是每一趟计数排序时间,我们知道,k不超过9,d的值一般也很小,k、d都可以看成是一个很小的常数,所以时间复杂度O(n)。最坏最佳情况并不改变时间复杂度。从这里可以看出计数排序的稳定性直接影响了基数排序的结果,基数排序是稳定的。基数排序需要一部分辅助空间,所以在内存紧张的系统中,原地排序算法可能是更好的选择。

三、桶排序
    原理:
桶排序与计数排序类似,都是做了一种假设。桶排序是假设待排序序列是随机产生,并且均匀的分布在[0,1)的区间。其思想是将区间[0,1)划分成那个区间(或称桶),将n个序列元素分不到桶中。若多个元素分不到同一个桶中,则需要进行桶内排序。最后将各桶内数据依次输出得到有序序列。
    代码实现:下面的以小数点后第一位作为区间划分的标准,共10个区间。

点击(此处)折叠或打开

  1. // 链表节点结构体
  2. typedef struct Node
  3. {
  4.     double key;
  5.     struct Node *next;
  6. }Node;
  7. /************************************************************************/
  8. /* 桶排序 */
  9. /************************************************************************/
  10. void BucketSort(double *a, int n)
  11. {
  12.     Node *List[10] = {NULL}; // 申请10个桶
  13.     int i = 0, j = 0;
  14.     Node *node = NULL;
  15.     Node *p = NULL;
  16.     Node *q = NULL;

  17.     // 循环待排序数组,放到桶的相应位置
  18.     for(i = 0; i < n; i++)
  19.     {
  20.         node = calloc(1, sizeof(Node));
  21.         node->key = a[i];
  22.         node->next = NULL;

  23.         // 桶中没有元素
  24.         q = p = List[(int)(a[i] * 10)];
  25.         if(p == NULL)
  26.         {
  27.             List[(int)(a[i] * 10)] = node;
  28.             continue;
  29.         }
  30.         
  31.         // 桶中有元素则插入到相应位置
  32.         while(p)
  33.         {
  34.             if(a[i] < p->key)
  35.             {
  36.                 q->next = node;
  37.                 node->next = p;
  38.                 break;
  39.             }

  40.             q = p;
  41.             p = p->next;
  42.         }

  43.         if(p == NULL)
  44.         {
  45.             q->next = node;
  46.         }
  47.     }

  48.     // 将桶中元素输出到a中
  49.     for(i = 0; i < 10; i++)
  50.     {
  51.         p = List[i];
  52.         while(p)
  53.         {
  54.             a[j++] = p->key;
  55.             q = p;
  56.             p = p->next;
  57.             free(q);
  58.         }
  59.     }

  60. }
    性能分析:在最好情况下,当记录在桶中分布均匀时,即每个桶只有一个元素,此时时间复杂度O(n)。因此桶排序适合对很少重复的记录排序。辅助空间n。而且桶排序是稳定的排序,实现比较复杂。
    从桶排序的辅助空间可以看出,这是类似于hash表的数据结构,桶内元素的插入和遍历是线性顺序遍历的;所以当桶内元素过多时插入的效率是很低的。在tomcat早期版本中hash表的相同元素的遍历采用了线性查找、插入算法,导致了旧版本的tomcat很容易造成ddos攻击,这点需要注意。在这个例子中采用了小数点后一位作为桶的划分标准,在实际应用中可以采用其他粒度作为桶划分的标准,如小数点2位、hash值等等。
阅读(2257) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~