代码改变世界

桶排序和鸽巢排序

2013-11-06 15:56  youxin  阅读(1255)  评论(0编辑  收藏  举报

桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θn))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。

桶排序以下列程序进行:

  1. 设置一个定量的数组当作空桶子。
  2. 寻访串行,并且把项目一个一个放到对应的桶子去。
  3. 对每个不是空的桶子进行排序。
  4. 从不是空的桶子里把项目再放回原来的串行中。

 

function bucket-sort(array, n) is
  buckets ← new array of n empty lists
  for i = 0 to (length(array)-1) do
    insert array[i] into buckets[msbits(array[i], k)]
  for i = 0 to n - 1 do
    next-sort(buckets[i])
  return the concatenation of buckets[0], ..., buckets[n-1]

桶排序的基本思想

       设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]....B[M]中的全部内容即是一个有序序列。

 

[桶—关键字]映射函数

      bindex=f(key)   其中,bindex 为桶数组B的下标(即第bindex个桶), k为待排序列的关键字。桶排序之所以能够高效,其关键在于这个映射函数,它必须做到:如果关键字k1<k2,那么f(k1)<=f(k2)。也就是说B(i)中的最小数据都要大于B(i-1)中最大数据。很显然,映射函数的确定与数据本身的特点有很大的关系,我们下面举个例子:

 

假如待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。这些数据全部在1—100之间。因此我们定制10个桶,然后确定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将所有关键字全部堆入桶中,并在每个非空的桶中进行快速排序后得到如下图所示:

对上图只要顺序输出每个B[i]中的数据就可以得到有序序列了。

 

桶排序代价分析

桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。

 

对N个关键字进行桶排序的时间复杂度分为两个部分:

(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。

(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为  ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。

 

很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:

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

(2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。

 

对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:

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

当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。

 

总结: 桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。 当然桶排序的空间复杂度 为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。

 

其实我个人还有一个感受:在查找算法中,基于比较的查找算法最好的时间复杂度也是O(logN)。比如折半查找、平衡二叉树、红黑树等。但是Hash表却有O(C)线性级别的查找效率(不冲突情况下查找效率达到O(1))。大家好好体会一下:Hash表的思想和桶排序是不是有一曲同工之妙呢?

 

桶排序在海量数据中的应用

 

一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,你把这500 万元素的数组排个序。

 

分析:对500W数据排序,如果基于比较的先进排序,平均比较次数为O(5000000*log5000000)≈1.112亿。但是我们发现,这些数据都有特殊的条件:  100=<score<=900。那么我们就可以考虑桶排序这样一个“投机取巧”的办法、让其在毫秒级别就完成500万排序。

 

方法:创建801(900-100)个桶。将每个考生的分数丢进f(score)=score-100的桶中。这个过程从头到尾遍历一遍数据只需要500W次。然后根据桶号大小依次将桶中数值输出,即可以得到一个有序的序列。而且可以很容易的得到100分有***人,501分有***人。

 

实际上,桶排序对数据的条件有特殊要求,如果上面的分数不是从100-900,而是从0-2亿,那么分配2亿个桶显然是不可能的。所以桶排序有其局限性,适合元素值集合并不大的情况。

#include<iostream>
using namespace std;

typedef struct Node{
    int key;
    struct Node* next;
    Node():key(0),next(NULL){}
    Node(int m_key,struct Node* m_next):key(m_key),next(m_next){}
}*LinkList;

void bucketSort(int a[],int n,int bucketSize)
{
    LinkList *table=new LinkList[bucketSize];
    for(int i=0;i<bucketSize;i++)
    {
        table[i]=new Node();//table[i]为头结点 
         
    }
    for(int i=0;i<n;i++)
    {
        Node *node=new Node(a[i],NULL);

        //映射函数计算桶号  
        int index=a[i]/10;

      //初始化P成为桶中数据链表的头指针  
        Node* p=table[index];
         //该桶中还没有数据  
        if(p->key==0)
        {
            table[index]->next=node;
            (table[index]->key)++;
        }
        else
        {
            //链表结构的插入排序 
            while(p->next!=NULL && p->next->key<=node->key)
                p=p->next;
            node->next=p->next;
            p->next=node;

            (table[index]->key)++;
        }
    }
    //打印结果
    for(int i=0;i<bucketSize;i++)
        for(Node *p=table[i]->next;p!=NULL;p=p->next)
            cout<<p->key<<ends;
        
}

int main()
{
    int a[]={5,4,3,3,2,2,1};
    int n=sizeof(a)/sizeof(a[0]);
    bucketSort(a,n,5);
}

 

 上面源代码的桶内数据排序,我们使用了基于单链表的直接插入排序算法。可以使用基于双向链表的快排算法提高效率。

参考了:http://hxraid.iteye.com/blog/647759

可以参考:http://www.cnblogs.com/lpshou/archive/2012/06/18/2553370.html

http://blog.csdn.net/xiajun07061225/article/details/8731179

 

----------------------------------------------------------------------------------------------------------

鸽巢排序(Pigeonhole sort), 也被称作基数分类, 是一种时间复杂度O(n)且在不可避免遍历每一个元素并且排序的情况下效率最好的一种排序算法. 但它只有在差值(或者可被映射在差值)很小的范围内的数值排序的情况下实用.

当涉及到多个不相等的元素, 且将这些元素放在同一个"鸽巢"的时候, 算法的效率会有所降低.为了简便和保持鸽巢排序在适应不同的情况, 比如两个在同一个存储桶中结束的元素必然相等

我们一般很少使用鸽巢排序, 因为它很少可以在灵活性, 简便性, 尤是速度上超过其他排序算法. 事实上, 桶排序较鸽巢排序更加的实用.

鸽巢排序的一个比较有名的变形是tally sort, 它仅仅适用非常有限的题目, 这个算法因在Programming Pearls一书中作为解决一个非常规有限集问题方法的例子而著名.

显然, 快速排序可以当作只有两个(有些情况下是三个)"鸽巢"的鸽巢排序

对于N个不同元素的鸽巢排序算法(伪代码)

 function pigeonhole_sort(array a[n])
      array b[N]
      var i,j
      
      zero_var (b)      (* Zero out array b *)
      
      for i in [0...length(a)-1]
          b[a[i]] := b[a[i]]+1
   
      (* 把结果复制回数组a *)
      j := 0
      for i in [0...length(b)-1]
          repeat b[i] times
             a[j] := i
             j := j+1

经典排序算法 - 鸽巢排序Pigeonhole sort

原理类似桶排序,同样需要一个很大的鸽巢[桶排序里管这个叫桶,名字无所谓了]

鸽巢其实就是数组啦,数组的索引位置就表示值,该索引位置的值表示出现次数,如果全部为1次或0次那就是桶排序

例如

var pigeonHole = new int[100];

pigeonHole[0]的值表示0的出现次数...

pigeonHole[1]的值表示1的出现次数...

pigeonHole[2]的值表示2的出现次数...

参考http://hi.baidu.com/wangxvfeng101/blog/item/a2c22560e57260c58cb10d8c.html

Pigeonhole sorting, also known as count sort (not to be confused with counting sort), is a sorting algorithm that is suitable for sorting lists of elements where the number of elements (n) and the number of possible key values (N) are approximately the same.[1] It requires O(n + N) time.

The pigeonhole algorithm works as follows:

  1. Given an array of values to be sorted, set up an auxiliary array of initially empty "pigeonholes," one pigeonhole for each key through the range of the original array.
  2. Going over the original array, put each value into the pigeonhole corresponding to its key, such that each pigeonhole eventually contains a list of all values with that key.
  3. Iterate over the pigeonhole array in order, and put elements from non-empty pigeonholes back into the original array.
#include<iostream>
using namespace std;

int * sort(int a[],int n,int maxNumber=10)
{
    int *hole=new int[maxNumber+1];
    for(int i=0;i<=maxNumber;i++)
        hole[i]=0;

    for(int i=0;i<n;i++)
        hole[a[i]]++;

    return hole;
    /*
             * p Hole[10] = 4; 的含意是
             * 在待排数组中有4个10出现,同理其它
             */
}
int main()
{
    int a[]={5,4,3,3,1};
    int n=sizeof(a)/sizeof(a[0]);
    int maxNumber=5;
    int *b=sort(a,n,5);
    for(int i=0;i<=5;i++)
        cout<<b[i]<<ends; //结果为0 1 0 2 1 1
    cout<<endl;

    for(int i=0;i<=maxNumber;i++) 
        for(int j=0;j<b[i];j++)
            cout<<i<<ends;
}

参考了:http://www.cnblogs.com/kkun/archive/2011/11/23/2260273.html