算法与数据结构-常用排序算法总结2-桶排序

来源:互联网 发布:心动网络校招笔试 编辑:程序博客网 时间:2024/06/04 18:16

序言

排序算法大体可分为两种:

比较排序:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等非比较排序:基数排序,计数排序,桶排序等

本文介绍非比较排序算法中的桶排序算法。


桶排序(Bucket Sort)

  • 原理

    • 又称箱排序,原理为:将数组元素分到有限数量的桶子里,每个桶子再单独进行排序

      • 单独排序可能使用别的排序算法(比较排序算法)或是以递归方式继续使用桶排序进行排序
    • 桶排序的思想:映射函数

      • 假设有M个桶,N个数据待排,将待排序列的关键字映射到某一个桶中,该关键字即该桶中的一个元素。

      • 一个桶中可能有多个元素,桶内可使用比较排序算法(快排、插入排序等)或者继续递归桶排序

      • 映射函数举例说明:

        • 假如待排序列K = {49、 38 、 35、 97 、 76、 73 、 27、 49 }
        • 这些数据全部在1—100之间。因此我们定制10个桶,然后确定映射函数f(k) = k/10
        • 那么,第一个关键字49将定位到第4个桶中(49/10 = 4)
        • 依次将所有关键字全部堆入桶中,并在每个非空的桶中进行快速排序
        • 最后,顺序输出每个桶中的数据就可以得到有序序列了
    • 动态示意图:http://www.cs.usfca.edu/~galles/visualization/BucketSort.html


  • 步骤

    • 设置一个定量的数组当作空桶

    • 根据映射关系,将数组元素一个个放入对应的桶中

    • 对每个不为空的桶进行单独排序(冒泡排序,插入排序,快速排序,归并排序等)

    • 将不为空的桶中将元素放回原数组中


  • 要点

    • 映射函数/关系的设计

    • 桶内排序


  • 时间复杂度分析

    • 桶排序的时间复杂度分为两部分(假设元素个数为n,桶个数为M)

      • 循环计算每个元素的桶映射函数/关系,这个时间复杂度为O(N)

      • 假设桶内排序使用时间复杂度为O(N*logN),这个时间复杂度为∑O(Ni*logNi)

    • 显然,桶内排序是桶排序性能好坏的决定因素。

      • 尽量减少桶内数据的数量是提高效率的唯一办法,基于比较排序的最好平均时间复杂度只能达到O(N*logN)。为了减小时间复杂度,需尽量做到下面两点:

        • (1)映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量

          • (桶内元素分布不平衡将导致算法效率大大降低,如果所有数据都落在同一个桶中,则退化为一般的比较排序了)
        • (2)尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,避开桶内数据的“比较”排序操作

      • 桶数量越多,意味着空间浪费越大,所以在时间和空间复杂度之间需要做出权衡

    • 最优时间复杂度:O(N)

      • 假设N个待排数据,M个桶,平均每个桶N/M个数据,则时间复杂度为

      • O(N) + O(M*(N/M)*log(N/M)) = O(N + N*(logN - logM))

      • 当N = M时,即极限情况下每个桶一个数据,桶排序的最优时间复杂度能达到:O(N)

    • 平均时间复杂度:O(N + C)

      • 其中C = N*(logN - logM)

      • M越大,效率越高。“桶越多,效率越高


  • 空间复杂度分析

    • 桶排序的空间复杂度为:O(N + M)

    • 当输入数据量很大,桶数量也非常多时,空间代价相应也增大


  • 评价

    • 桶排序是稳定的

    • 桶排序利用函数的映射关系,减少了几乎所有的比较工作。

      • 实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分希尔排序中的子序列归并排序中的子问题,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。 
    • 计数排序桶排序比较:

      • 桶排序算是计数排序的一种改进和推广,比计数排序更为复杂
    • 基数排序桶排序的比较:

      • 基数排序的性能比桶排序要略差。

        • 每一次关键字的桶分配都需要O(N)的时间复杂度,而且分配之后得到新的关键字序列又需要O(N)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2N) ,当然d要远远小于N,因此基本上还是线性级别的。基数排序的空间复杂度为O(N+M),其中M为桶的数量。一般来说N>>M,因此额外空间需要大概N个左右。
      • 但是,对比桶排序,基数排序每次需要的桶的数量并不多。而且基数排序几乎不需要任何“比较”操作,而桶排序在桶相对较少的情况下,桶内多个数据必须进行基于比较操作的排序。因此,在实际应用中基数排序的应用范围更加广泛。


Code Example

/**************************************功能:桶排序参数:    pos:第pos位    n:数组元素个数**************************************/    #include <stdio.h>       #include <stdlib.h>       #include <string.h>       #define N 10            //数组元素个数    struct barrel    {           int node[N];        int count;          //元素个数    };       /* 输出函数 */    void  PrintArr(int arr[],int n)    {        for(int i = 0; i < n; ++i)            printf("%d ", arr[i]);          printf("\n");    }    /* 直接插入排序 */    void InsertionSort(int array[], int count)    {        for (int i = 0; i < count; i++)        {            int temp = array[i];            int j = i;            while (j - 1 >= 0 && temp < array[j - 1])            {                array[j] = array[j - 1];                j--;            }            array[j] = temp;        }    }    /* 桶排序函数 */    void BucketSort(int data[], int n)       {           int max, min, bucket_num, pos;           int i, j, k;           struct barrel *pBarrel;           //提取数组最大最小值,确定映射关系        max = min = data[0];           for (i = 1; i < n; i++)         {               if (data[i] > max)             {                   max = data[i];               }             else if (data[i] < min)             {                   min = data[i];               }           }           bucket_num = (max - min + 1) / 10 + 1;        //桶空间分配即初始化           pBarrel = (struct barrel*)malloc(sizeof(struct barrel) * bucket_num);           memset(pBarrel, 0, sizeof(struct barrel) * bucket_num);           //数组元素放入桶内        for (i = 0; i < n; i++)         {               k = (data[i] - min + 1) / 10;       //应放入哪个桶内             (pBarrel + k)->node[(pBarrel + k)->count] = data[i];               (pBarrel + k)->count++;             //计数        }           //桶内排序           pos = 0;           for (i = 0; i < bucket_num; i++)         {               //直接插入排序            InsertionSort((pBarrel+i)->node, (pBarrel+i)->count);            //桶内排序完成即输出            for (j = 0; j < (pBarrel+i)->count; j++)             {                   data[pos++] = (pBarrel+i)->node[j];               }           }           free(pBarrel);       }       int main()       {           int data[] = {78, 17, 39, 26, 72, 94, 21, 12, 23, 91}, i;           int n = sizeof(data) / sizeof(int);           //函数调用        BucketSort(data, n);           //输出        PrintArr(data, n);        return 0;    }  

注:

  • 以上桶排序算法元素采用结构体方式存储,为了易于变更和操作,在实际应用中可采用链表方式,可参考文章:http://hxraid.iteye.com/blog/647759

  • 桶内排序使用了直接插入排序,可修改为其他比较排序算法,比如快排、归并、冒泡等



Acknowledgements:
http://blog.csdn.net/han_xiaoyang/article/details/12163251#t139(推荐,桶排序的应用实例)
http://blog.csdn.net/houapple/article/details/6480100
https://segmentfault.com/a/1190000003054515

2017.08.09