应用场景
数列取值范围过大,或者不为整数时,不再适用计数排序,升级为桶排序来进行排序。
题型:大数据时代:本月某网站播放数量前十的视频,视频数量又多、播放次数差别又大,非常适合桶排序。
算法原理
类别于计数排序中计数数组的某一个元素,桶排序中的每一个桶代表了一个区间范围,里面可以承载一个或多个元素。桶排序的第一步就是创建这些桶,并确定每一个桶的区间范围。
举例:2.2,0.5,5.5,4.24,0.54,3.36
具体创建多少个桶,如何确定桶的区间范围,有很多不同的方式。面对少量数据,可以创建和原始数据量相同的桶数量,除了最后一个桶只包含数列的最大值,其余的每个桶的区间按比例分配。
区间跨度 = ( 最大值 - 最小值 ) / ( 桶的数量 - 1 )
桶数:6
区间跨度 = ( 5.5 - 0.5 ) / ( 6 - 1) = 1
桶区间:[0.5,1.5),[1.5,2.5),[2.5,3.5),[3.5,4.5),[4.5,5.5),[5.5,5.5]
桶排序的第二步就是遍历原始数列,然后把元素对号入座放入各个桶中。
比如 0.5 放入第一个桶,2.2 放入第二个桶,5.5 放入最后一个桶,依次类推,注意区间开闭
桶一:0.5,0.54 桶二:2.2
桶三:3.36 桶四:4.24
桶五:无 桶六:5.5
桶排序的第三步就是对桶内元素分别排序即可。桶内排序在数据量较小时可以使用 直接插入排序 ,数据量较大时使用 快速排序 。
比如这里只需要对第一个桶进行排序,得到第一个桶里顺序为 0.5,0.54
桶排序的第四步就是按顺序遍历所有桶,输出其中的所有元素。
最后的顺序为0.5,0.54,2.2,3.36,4.24,5.5
复杂度
对于 n 个数据,我们创建 m 个桶进行排序,则它的:
-
时间复杂度:
①遍历求最大值,O(n);
②创建m个空桶,O(m);
③遍历原始序列,O(n);
④每个桶内部排序,运算量为 m*(n / m)*log(n / m) ,即时间复杂度为 O(n(logn - logm)) ,当然,这是在序列在桶间分布非常均匀的情况下。
⑤输出排序后的序列,O(n);
总的时间复杂度为 O(3n + m + n(logn - logm))。因此:
在最好的情况下,n 个数据创建 n 个桶,每个桶内都装一个元素,可知算法的时间复杂度为 O(3n+n+n(logn - logn)),即退化为 O(n) 时间复杂度;
在最坏的情况下,n 个数据创建 m 个桶,除了最后一个桶有最大值外,其余数据全部集中在第一个桶内,则第一个桶内排序时间复杂度为 O(n * logn) ,即退化为 O(n * logn) 时间复杂度,还会多创建 m - 2 个空桶。 -
空间复杂度:O(m + n)
代码实现
public void bucketSort(double[] nums){
double max = nums[0],min = nums[0];
//第一趟遍历,获得最大值和最小值
for(int i=1;i<nums.length;i++){
if(nums[i] > max)
max = nums[i];
if(nums[i] < min)
min = nums[i];
}
//获取跨度值
double d = max - min;
//创建桶
int bucketNum = nums.length;
ArrayList<LinkedList<Double>> bucketList = new ArrayList<>();
for (int i=0;i<bucketNum;i++){
bucketList.add(new LinkedList<Double>());
}
//将数字放入对应桶中
for(double num:nums){
//复杂度为O(1)的判断操作
int bucketNo = (int)((num-min)*(bucketNum-1)/d);
bucketList.get(bucketNo).add(num);
}
//对每个桶内元素进行排序
for(LinkedList<Double> list : bucketList){
Collections.sort(list);
}
//将原数组进行排序输出
int index = 0;
for(LinkedList<Double> list : bucketList){
for(double num:list){
nums[index++] = num;
}
}
}
这里判断数字位于哪个桶中的方法,复杂度是O(1),需要记住这个方法。