1、什么是桶排序
桶排序是一种稳定的排序算法。它的工作原理是将序列中的元素分布到一定数量的桶内,然后分别对每个桶内的元素进行排序,最后再将各个桶内的有序子序列放回原始序列中。对于单个桶内的元素排序,我们可以使用别的排序算法,也可以递归使用桶排序。一般来说,对于单个桶内的元素,使用插入排序算法对它们进行排序。
2、问题
假设目前有包含 100,200,300,400,500,600 共 6 个数字的一个随机顺序的集合。我们需要对这个集合进行桶排序。
3、分析桶排序
桶排序需要思考两个问题,有多少个桶,每个桶多大也就是每个桶放多少。
首先我们设置桶的大小为 2 。
然后一共有6个数字 除以每个桶的 大小 就是需要多少个桶,在这里我们需要3个桶。
现在有桶了,我们需要一个方法找到元素对应的桶。我们使用一个计算公式来计算所属哪个桶:f(x) = (int)((x - min) / (max - min + 1.0) * 桶个数)。
(x - min) / (max - min + 1.0) 计算出的结果 是一个大于等于0,最大趋近于1且不等于1的值,然后乘以桶的个数进行强转整数类型,就可以计算出0-9之间的一个数字,就代表具体的桶编号。之所以用这个公式,是因为这个公式会随着 x 的变大而变大,可以保证桶内数据是在某个范围内,且后边的桶比前边的桶内存放的数据要大。
每个桶都存放完成后,遍历10个桶,每个桶都进行插入排序。排序完一个桶后就将这个桶中的数据放回原先的集合中。
4、步骤
- 设置几个数组作为空桶。
- 从左到右遍历待排序序列,把每个元素都放到对应的桶中
- 对每个不是空的桶进行排序。
- 依次取出所有桶中的元素放回原序列
5、根据《算法》中描述每个桶的大小在 5~10 之间性能最优。
时间空间复杂度分析
空间复杂度
因为排序过程中用到了一个辅助桶来存储元素,所以空间复杂度是O(n) 。
时间复杂度
有 n 个待排序元素,均匀地将这些元素划分到 bucketCount 个桶内,每个桶里就有 k=n/bucketCount 个元素。每个桶内部使用快速排序,时间复杂度为 O(k logk)。m 个桶排序的时间复杂度就是 O(bucketCount k logk),因为 k=n/bucketCount,所以整个桶排序的时间复杂度就是 O(nlog(n/bucketCount))。当桶的个数 bucketCount 接近数据个数 n 时,log(n/bucketCount) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。所以时间复杂度为O(n)。
桶排序的适应场景
桶排序对数据要求较高,适用于数据是均匀分布的情况,这样可以让分布到各个桶内的元素数量相当。而不是被集中分配到其中一个桶或几个桶。
比较适合用在外部排序中。
因为数据量比较大,数据存储在外部磁盘中,无法一次性全部放入内存进行排序,一部分一部分的读入内存再写入磁盘,这种方式称为外部排序。
代码实现
Java实现代码
public class BucketSort {
private void insertionSort(List<Integer> arr) {
if (arr == null || arr.size() == 0) return;
for (int i = 1; i < arr.size(); ++i) {
int cur = arr.get(i);
int j = i - 1;
while (j >= 0 && arr.get(j) > cur) {
arr.set(j+1, arr.get(j));
--j;
}
arr.set(j+1, cur);
}
}
private int bucketSize;
public BucketSort(int bucketSize) {
this.bucketSize = bucketSize;
}
public void sort(int[] arr) {
if (arr == null || arr.length == 0) return;
int max = arr[0], min = arr[0];
for (int num: arr) {
if (num > max) max = num;
if (num < min) min = num;
}
int bucketCount = arr.length / bucketSize;
List<List<Integer>> buckets = new ArrayList<>(bucketCount);
for (int i = 0; i < bucketCount; ++i)
buckets.add(new ArrayList<>());
for (int num: arr) {
int idx = (int)((num - min) / (max - min + 1.0) * bucketCount);
buckets.get(idx).add(num);
}
int idx = 0;
for (List<Integer> bucket: buckets) {
insertionSort(bucket);
for (int num: bucket)
arr[idx++] = num;
}
}
}
JavaScript实现代码
def Bucket_Sort(array, bucketsize):
minValue = min(array)
maxValue = max(array)
res = []
bucketcount = (maxValue - minValue + 1) // bucketsize
bucket_lists = [[] for i in range(bucketcount)]
for i in array:
bucket_index = (i - minValue) // bucketsize
bucket_lists[bucket_index].append(i)
# 桶内排序
for j in bucket_lists:
Quick_Sort_2(j, 0, len(j)-1)
for j in bucket_lists:
if len(j) != 0:
res.extend(j)
return res
进阶之路
查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中。因为其实现代码较短,应用较常见。所以在面试中经常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。
一般在面试中最常考的是快速排序和归并排序,并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。
面试官对于这些排序可能会要求比较各自的优劣、各种算法的思想及其使用场景。还有要会分析算法的时间和空间复杂度。通常查找和排序算法的考察是面试的开始,如果这些问题回答不好,估计面试官都没有继续面试下去的兴趣了。
所以想开个好头就要把常见的排序算法思想及其特点要熟练掌握,有必要时要熟练写出代码。