基本原理
1.桶排序也是一种分而治之思想,其时间复杂度在最好的情况下比快排还要好,但是需要使用空间换区时间。
2.假设有一个长度为N的待排序的序列K[1….n],现在将这个序列划分为M个子区间,每个子区间叫一个桶,那么现在就有了M个桶,现在通过一个映射函数f(x)将K[i]映射到桶B[j]中,也就是K[i]成为了B[j]中的一个元素。然后对每个桶进行排序,这里可以选择继续递归使用桶进行数据集的规模减小,也可以选择如快速排序、选择排序等比较排序方法进行桶中元素的排序。
3.其实桶排序跟HashMap很像,都是使用一个函数f(x)将数据进行分组,不过这里需要注意的是对于f(x),需要满足f(ki)<=f(kj),当ki<=kj的时候。也就是说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]中的数据就可以得到有序序列了。
时间复杂度、空间复杂度
对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),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
注:稳定排序是指,在一个待排序序列中,存在两个或以上相等的关键字(ai,aj,..),如果排完序后,ai,aj…的相对位置没有发生变化,那么就是稳定的。
海量数据的桶排序
一年的全国高考考生人数为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亿个桶显然是不可能的。所以桶排序有其局限性,适合元素值集合并不大的情况。
伪代码
A:待排序序列
B:桶
BUCKET-SORT(A)
1. n = A.length
2. new B[n-1] //可以按照具体情况决定B的大小
3. for i=0 to n-1
4. insert A[i] into B[f(A[i])] //使用映射函数f(x)划分A
5. for i=0 to n-1
6. sort B[i]
图来自《算法导论》,仅说明桶的实现可以是数组+链表
java代码实现
//使用的排序方式是插入排序
public static void BucketSort(int[] num) {
int length = num.length;
Entry[] bucket = new Entry[10];
for (int i = 0; i < bucket.length; i++) {
bucket[i] = new Entry(); //初始化桶
}
for (int i = 0; i < length; i++) {
Entry node = new Entry(num[i], null);
int index = num[i] / 10;
if (bucket[index].next == null) {
bucket[index].next = node;
} else {
Entry head = bucket[index];
Entry temp = head.next;
while (temp != null) {
if (node.value < temp.value) {
//head.next = node;
break;
} else {
head = temp;
temp = temp.next;
}
}
head.next = node;
}
}
for (int i = 0; i < bucket.length; i++) {
Entry temp = bucket[i].next;
while (temp != null) {
System.out.print(temp.value + " ");
temp = temp.next;
}
}
}
参考资料:
【排序结构6】 桶排序
啊哈!算法