一、图书馆的故事:如何快速整理书籍
图书馆新到了 1000 本图书,需要按照书名的首字母(A-Z)进行分类。图书管理员想到了一个办法:
-
准备 26 个书架:每个书架对应一个字母(A 书架放 A 开头的书,B 书架放 B 开头的书,以此类推)
-
分类投放:将每本书放到对应的书架上
-
书架内排序:对每个书架上的书进行单独排序(比如按书名拼音)
-
合并结果:按书架顺序依次取下所有书,形成最终排序
这就是桶排序的核心思想:分桶 + 桶内排序 + 合并。
二、桶排序的 Java 代码实现
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class BucketSort {
public static void sort(double[] array) {
if (array == null || array.length == 0) return;
// 1. 确定桶的数量
int bucketCount = 10; // 假设分为10个桶
// 2. 创建桶
List<List<Double>> buckets = new ArrayList<>(bucketCount);
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
// 3. 将元素分配到桶中
double max = array[0];
double min = array[0];
for (double num : array) {
if (num > max) max = num;
if (num < min) min = num;
}
double range = (max - min) / bucketCount;
for (double num : array) {
int bucketIndex = (int) ((num - min) / range);
if (bucketIndex >= bucketCount) bucketIndex = bucketCount - 1; // 处理最大值
buckets.get(bucketIndex).add(num);
}
// 4. 对每个桶进行排序
for (List<Double> bucket : buckets) {
Collections.sort(bucket);
}
// 5. 合并所有桶的结果
int index = 0;
for (List<Double> bucket : buckets) {
for (double num : bucket) {
array[index++] = num;
}
}
}
}
三、桶排序的核心步骤解析
1. 确定桶的数量和范围
根据数据分布,确定合适的桶数量,并计算每个桶的范围:
java
// 示例数组:[0.42, 0.32, 0.33, 0.52, 0.37, 0.47, 0.51]
// 桶数量 = 5
// 范围计算:(0.52 - 0.32) / 5 = 0.04
// 桶0: [0.32, 0.36)
// 桶1: [0.36, 0.40)
// 桶2: [0.40, 0.44)
// 桶3: [0.44, 0.48)
// 桶4: [0.48, 0.52]
2. 将元素分配到桶中
根据元素的值,将其放入对应的桶:
java
// 分配结果:
// 桶0: [0.32, 0.33]
// 桶1: [0.37]
// 桶2: [0.42]
// 桶3: [0.47]
// 桶4: [0.52, 0.51]
3. 桶内排序
对每个桶内的元素进行排序(使用其他排序算法,如插入排序、快速排序):
java
// 排序后:
// 桶0: [0.32, 0.33]
// 桶1: [0.37]
// 桶2: [0.42]
// 桶3: [0.47]
// 桶4: [0.51, 0.52]
4. 合并结果
按桶的顺序依次取出元素,形成最终排序结果:
java
// 合并后:[0.32, 0.33, 0.37, 0.42, 0.47, 0.51, 0.52]
四、桶排序的性能分析
-
时间复杂度:O (n + k)(平均情况)
- n:元素个数
- k:桶的数量
最坏情况:O (n²)(所有元素集中在一个桶)
-
空间复杂度:O(n + k)
-
稳定性:取决于桶内排序算法(若使用稳定排序,则桶排序稳定)
-
适用条件:
- 数据均匀分布(避免桶内元素过多)
- 取值范围已知(便于确定桶的数量和范围)
五、桶排序的优化与变种
1. 动态调整桶数量
根据数据范围和分布动态确定桶的数量:
java
// 改进版:桶数量 = 元素个数的平方根
int bucketCount = (int) Math.sqrt(array.length) + 1;
2. 处理整数数据
对于整数,可以根据数值范围直接映射到桶:
java
// 示例:排序整数 [12, 23, 34, 45, 56, 67, 78]
// 桶0: [10-19] → [12]
// 桶1: [20-29] → [23]
// 桶2: [30-39] → [34]
// ...
3. 桶内排序算法选择
对于小规模数据,桶内可使用插入排序;对于大规模数据,可使用快速排序:
java
// 桶内排序优化
if (bucket.size() <= 10) {
insertionSort(bucket); // 小规模数据用插入排序
} else {
Collections.sort(bucket); // 默认使用归并排序(稳定)
}
六、桶排序 vs 其他排序算法
| 排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 桶排序 | O(n + k) | O(n + k) | 可选 | 数据均匀分布的场景 |
| 快速排序 | O(n log n) | O(log n) | 不稳定 | 通用场景 |
| 归并排序 | O(n log n) | O(n) | 稳定 | 大规模数据、链表排序 |
| 计数排序 | O(n + k) | O(k) | 稳定 | 数据范围小的整数排序 |
七、总结:桶排序的核心思想
桶排序的本质是 "分而治之":
-
分桶:将数据按范围分配到多个桶中
-
桶内排序:每个桶独立排序
-
合并:按桶的顺序合并结果
就像图书馆整理书籍,通过分类和子分类,将大规模排序问题转化为多个小规模排序问题。桶排序特别适合处理均匀分布的数据,效率远超基于比较的排序算法。但它需要预先了解数据分布,并合理设置桶的数量,否则可能导致性能下降。