Bucket Sort 桶排序
Bucket Sort 桶排序
核心结论
桶排序先依据关键字范围将元素分配到若干个有序的桶中,再分别排序每个桶,最后按桶的顺序连接结果。
当 个输入元素独立、均匀地分布在 上,使用 个桶并在桶内执行 插入排序 时,期望时间复杂度为 ;但当大量元素集中到同一个桶中时,最坏时间复杂度仍为 。
1. 问题定义
给定包含 个元素的数组:
桶排序要输出其非递减排列:
经典版本假设每个元素满足:
并建立 个桶:
元素 被映射到桶:
由于 ,所以:
2. 基本思想
桶排序利用了输入数据的数值分布信息,而不只是通过元素之间的比较确定顺序。
其核心思想可以概括为:
- 将整个关键字范围划分为若干个连续且有序的子区间,每个子区间对应一个桶。
- 根据元素的值,把每个元素放入对应的桶中。
- 分别排序每个桶内部的元素。
- 按桶编号从小到大依次连接各桶。
如果桶的划分满足:
那么在每个桶内部排好序之后,按桶编号连接即可得到全局有序序列。
本质
桶排序不是“把元素直接放到最终位置”,而是先完成一次粗粒度排序,再在每个桶中完成细粒度排序。
3. 具体过程
设输入数组长度为 ,输入元素均位于 。
3.1 创建桶
创建 个空桶:
桶 对应区间:
3.2 将元素分配到桶中
对于每个元素 ,计算:
并将 插入桶 。
3.3 对每个桶分别排序
对每个桶 内部执行排序。
经典分析通常采用插入排序,因为在线性期望时间成立的条件下,每个桶的期望元素数量为常数,插入排序处理小规模数组的常数开销较低。
3.4 按桶编号连接
依次连接:
连接后的序列就是最终排序结果。
graph LR
A[输入数组] --> D[根据桶映射函数分配元素]
D --> B0[桶 0]
D --> B1[桶 1]
D --> B2[...]
D --> B3[桶 k-1]
B0 --> S0[桶内排序]
B1 --> S1[桶内排序]
B2 --> S2[桶内排序]
B3 --> S3[桶内排序]
S0 --> C[按桶编号连接]
S1 --> C
S2 --> C
S3 --> C
C --> R[有序数组]
4. 伪代码
4.1 经典版本
下面使用 0-based 下标,并假设 。
BUCKET-SORT(A)
n ← length(A)
B ← array of n empty lists
for i ← 0 to n - 1
bucket_index ← floor(n × A[i])
append A[i] to B[bucket_index]
for i ← 0 to n - 1
INSERTION-SORT(B[i])
result ← empty list
for i ← 0 to n - 1
append every element of B[i] to result
return result
4.2 适用于任意数值区间的版本
若输入位于已知区间 ,且建立 个桶,可以使用归一化映射:
其中外层的 用于保证最大值恰好等于 时不会越界。
BUCKET-SORT-RANGE(A, k, minValue, maxValue)
B ← array of k empty lists
if length(A) ≤ 1
return A
if minValue = maxValue
return copy of A
for each x in A
normalized ← (x - minValue) / (maxValue - minValue)
bucket_index ← min(k - 1, floor(k × normalized))
append x to B[bucket_index]
result ← empty list
for i ← 0 to k - 1
sort B[i]
append every element of B[i] to result
return result
5. 正确性说明
桶排序的正确性依赖两个条件。
5.1 桶间有序性
桶映射函数必须保持数值区间的顺序。对于任意桶编号 :
因此,较小编号桶中的任何元素都不会大于较大编号桶中的元素。
5.2 桶内有序性
对每个桶 排序之后,桶内元素满足非递减顺序。
综合以上两点:
- 同一个桶内的元素已经有序;
- 不同桶之间按照值域区间天然有序。
所以按 的顺序连接,得到的整个数组必然有序。
循环不变式
在连接阶段开始处理桶 前,
result中已经包含桶 的全部元素,并且这些元素整体有序;同时,它们均不大于尚未连接桶中的任何元素。
6. 时间复杂度
设:
- 输入元素数量为 ;
- 桶的数量为 ;
- 第 个桶中的元素数量为 ;
- 满足 。
6.1 一般形式
创建桶需要:
分配全部元素需要:
连接全部桶需要:
若桶内采用插入排序,第 个桶的排序时间为:
因此总时间为:
6.2 期望时间复杂度
经典模型取 ,并假设 个输入元素独立、均匀地分布在 。
此时,每个元素落入任意一个桶的概率均为:
对任意桶 ,桶中元素数量 服从二项分布:
因此:
且:
所以:
最终得到:
期望线性时间的前提
是在特定概率模型下得到的期望复杂度,不是对任意输入都成立的最坏情况保证。
6.3 最坏时间复杂度
如果所有元素都进入同一个桶,则:
其余桶为空。若桶内使用插入排序:
因此:
6.4 最好时间复杂度
如果元素被均匀地分散到各个桶中,使每个桶只包含常数个元素,则:
当 时:
6.5 复杂度汇总
| 情况 | 桶内采用插入排序 | 条件 |
|---|---|---|
| 最好时间 | 元素均匀分散,单桶规模为常数 | |
| 期望时间 | ,输入独立均匀分布 | |
| 最坏时间 | 所有元素集中到一个桶 | |
| 空间复杂度 | 存储桶结构及全部元素 |
当 时,空间复杂度为:
7. 需要问题具有的性质
桶排序适用的关键不在于元素是否为整数,而在于能否设计出合理的桶划分。
7.1 关键字具有可划分的有序范围
必须能够根据元素关键字将其映射到有序桶中。例如:
- 内的浮点数;
- 已知最小值与最大值的实数;
- 年龄、成绩、价格等具有明确区间的数据;
- 可按前缀、区间或分位点划分的复合关键字。
7.2 桶映射必须保持顺序
若 ,映射结果应满足:
否则,按照桶编号连接时无法保证全局有序。
7.3 数据应尽可能均匀地分布到各桶
要获得接近线性的运行时间,应避免少数桶包含大量元素。
经典的 期望时间要求输入元素独立、均匀地分布在 。实际数据不一定严格均匀,但桶边界应尽量使各桶负载均衡。
非均匀分布的处理
对偏斜分布,可使用非等宽桶、分位数桶、直方图估计或自适应桶,使各桶包含的元素数量更加均衡。
7.4 桶的数量需要合理
桶数 过小:
- 单桶元素过多;
- 桶内排序成本增大。
桶数 过大:
- 初始化和遍历空桶的成本增大;
- 额外空间消耗增大。
经典分析通常选择:
实际实现中, 应结合数据规模、分布和内存限制确定。
7.5 桶内必须能够执行排序
桶内可以使用:
8. 典型例子
给定数组:
数组长度:
建立 个桶,桶编号为 到 ,映射函数为:
8.1 分配元素
| 元素 | 所属桶 | |
|---|---|---|
分配完成后:
B[0] = []
B[1] = [0.17, 0.12]
B[2] = [0.26, 0.21, 0.23]
B[3] = [0.39]
B[4] = []
B[5] = []
B[6] = [0.68]
B[7] = [0.78, 0.72]
B[8] = []
B[9] = [0.94]
8.2 桶内排序
B[0] = []
B[1] = [0.12, 0.17]
B[2] = [0.21, 0.23, 0.26]
B[3] = [0.39]
B[4] = []
B[5] = []
B[6] = [0.68]
B[7] = [0.72, 0.78]
B[8] = []
B[9] = [0.94]
8.3 连接各桶
按照桶编号从小到大连接:
9. Python 实现
9.1 经典 版本
from __future__ import annotations
def insertion_sort(values: list[float]) -> None:
"""使用插入排序原地排列一个桶。"""
for i in range(1, len(values)):
key = values[i]
j = i - 1
while j >= 0 and values[j] > key:
values[j + 1] = values[j]
j -= 1
values[j + 1] = key
def bucket_sort(values: list[float]) -> list[float]:
"""排序位于区间 [0, 1) 内的浮点数。"""
n = len(values)
if n <= 1:
return values.copy()
if any(value < 0.0 or value >= 1.0 for value in values):
raise ValueError("经典桶排序要求所有元素位于区间 [0, 1) 内")
buckets: list[list[float]] = [[] for _ in range(n)]
for value in values:
bucket_index = int(n * value)
buckets[bucket_index].append(value)
result: list[float] = []
for bucket in buckets:
insertion_sort(bucket)
result.extend(bucket)
return result
if __name__ == "__main__":
data = [0.78, 0.17, 0.39, 0.26, 0.72,
0.94, 0.21, 0.12, 0.23, 0.68]
sorted_data = bucket_sort(data)
print("原数组:", data)
print("排序后:", sorted_data)
assert sorted_data == sorted(data)
预期输出:
原数组: [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]
排序后: [0.12, 0.17, 0.21, 0.23, 0.26, 0.39, 0.68, 0.72, 0.78, 0.94]
9.2 任意数值范围版本
from __future__ import annotations
def bucket_sort_range(
values: list[float],
bucket_count: int | None = None,
) -> list[float]:
"""对任意有限浮点数范围执行桶排序。"""
n = len(values)
if n <= 1:
return values.copy()
if bucket_count is None:
bucket_count = n
if bucket_count <= 0:
raise ValueError("bucket_count 必须为正整数")
min_value = min(values)
max_value = max(values)
if min_value == max_value:
return values.copy()
buckets: list[list[float]] = [
[] for _ in range(bucket_count)
]
value_range = max_value - min_value
for value in values:
normalized = (value - min_value) / value_range
bucket_index = min(
bucket_count - 1,
int(bucket_count * normalized),
)
buckets[bucket_index].append(value)
result: list[float] = []
for bucket in buckets:
bucket.sort()
result.extend(bucket)
return result
if __name__ == "__main__":
data = [42.0, -3.5, 17.2, 8.8, 42.0, 0.0, 15.1]
result = bucket_sort_range(data)
print(result)
assert result == sorted(data)
10. 稳定性与原地性
10.1 稳定性
桶排序本身是否稳定取决于两个实现细节:
- 元素进入桶时是否保持原相对次序;
- 桶内排序算法是否稳定。
若采用尾部追加元素、稳定的桶内排序,并按桶编号顺序连接,则桶排序可以是稳定排序。
因此:
10.2 原地性
标准桶排序需要额外维护多个桶,并存储所有输入元素,因此通常不是原地排序:
11. 优点与局限
11.1 优点
- 当数据分布适合桶划分时,可以获得期望 的运行时间。
- 各桶可以独立排序,适合并行化。
- 对浮点数、区间型数据和近似均匀分布数据较有效。
- 桶内数据规模较小时,可利用插入排序的低常数开销。
11.2 局限
- 性能高度依赖输入分布和桶划分方式。
- 数据严重偏斜时可能退化为 。
- 需要 的额外空间。
- 需要提前知道或估计关键字范围及分布。
- 桶数选择不合理会导致时间或空间浪费。
12. 与其他线性时间排序的比较
| 算法 | 主要依据 | 典型适用对象 | 时间复杂度 | 是否依赖分布 |
|---|---|---|---|---|
| 计数排序 | 统计每个离散键出现次数 | 小范围整数 | 依赖键值范围大小 | |
| 基数排序 | 按位依次稳定排序 | 定长整数或字符串 | 依赖位数与每位取值范围 | |
| 桶排序 | 按值域区间分桶 | 区间内近似均匀分布的数据 | 期望 | 强烈依赖数据分布 |
桶排序与计数排序有什么区别?
- 计数排序通常为每一个离散键值建立计数位置,不需要在每个位置中再次排序。
- 桶排序通常让一个桶对应一个值域区间,一个桶中可以包含多个不同值,因此需要桶内排序。
13. 常见错误
错误 1:把所有桶排序都写成
只有在输入分布与桶划分满足相应条件时,桶排序才具有线性期望时间。最坏情况仍可能为 。
错误 2:直接使用
处理任意范围数据 该映射只适用于已经归一化到 的数据。任意范围数据需要先归一化。
错误 3:忽略最大值造成的下标越界
对闭区间 ,最大值归一化后等于 ,必须将桶编号限制到 。
错误 4:桶映射不保持顺序
桶编号必须与关键字大小单调一致,否则连接各桶不能得到全局有序结果。
14. 总结
桶排序的算法框架为:
其复杂度的一般形式为:
其中, 是第 个桶中的元素数量。
当 且输入独立均匀分布时:
当所有元素集中在一个桶中时:
15. 相关笔记
16. 参考资料
- Cormen, Thomas H., Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. 2009. Introduction to Algorithms, 3rd ed. Chapter 8.4, “Bucket Sort.” MIT Press.
- Ango, Steph. “Obsidian Flavored Markdown Skill.” kepano/obsidian-skills. GitHub.