不比较,直接按数值"对号入座"——用空间换时间,换来一种完全不同的排序思路。
前面三种排序(冒泡、选择、插入)有一个共同点:都靠"两两比较"决定谁该排在前面。桶排序换了一个完全不同的思路——它根本不比较任何两个数,而是直接利用数值本身的大小,把每个数"扔"进对应的桶里,最后按桶的顺序把所有数倒出来,就是排好序的结果。
这其实就是 2.2 节"数组标记法"里统计出现次数的直接应用:开一个数组当"桶",下标对应数值,每个桶记录这个数值出现了多少次。统计完之后,从头到尾扫一遍桶,每个桶里有几个数,就连续输出几次——自然就是从小到大排好的顺序。
用数组 {7, 2, 9, 2, 5, 2, 9} 演示(数值范围是 0~9):
| 1 | void BucketSort(int arr[], int n, int maxValue) |
| 2 | { |
| 3 | vector<int> bucket(maxValue + 1, 0); // 每个桶记录该数值出现的次数 |
| 4 | for (int i = 0; i < n; i++) |
| 5 | { |
| 6 | bucket[arr[i]]++; // 这个数值对应的桶计数加一 |
| 7 | } |
| 8 | |
| 9 | int index = 0; |
| 10 | for (int value = 0; value <= maxValue; value++) // 按桶的顺序,从小到大倒出来 |
| 11 | { |
| 12 | while (bucket[value] > 0) |
| 13 | { |
| 14 | arr[index] = value; |
| 15 | index++; |
| 16 | bucket[value]--; |
| 17 | } |
| 18 | } |
| 19 | } |
2 出现了 3 次),桶 5、7 各 1 个,桶 9 有 2 个,其余桶是空的。按桶的编号从 0 到 9 顺序读出来:2 2 2 5 7 9 9——完全不需要做任何比较,排序就完成了。桶排序快得不可思议——只需要扫一遍原数组(统计),再扫一遍桶数组(按顺序倒出来),总共大约 n + maxValue 次操作,比前面几种排序的 O(n²) 快得多。但它有一个隐藏的代价:桶的数量取决于数值的范围,不是数据的个数。
5 个数,但它们的取值范围是 0 到 10 亿,按这一节的写法就要开一个 10 亿大小的桶数组——内存根本装不下,而且绝大多数桶都是空的,纯属浪费。桶排序只适合"数值范围本身不会比数据量大太多"的场景,比如给 0~100 分的考试成绩排序、给 1~6 点的骰子结果排序。1000 个,数值范围是 0~100 时桶排序又快又省,范围一旦变成 0~10亿,所需要的桶数组大小会直接超出内存限制——这正是桶排序"用空间换时间"必须权衡的地方:空间够、范围小,桶排序是神器;空间不够、范围大,桶排序根本用不了。| 排序方式 | 核心操作 | 时间复杂度 | 额外空间 |
|---|---|---|---|
| 冒泡 / 选择 / 插入 | 两两比较、挪动 | O(n²) | 几乎不需要 |
| 桶排序 | 按数值直接归位,无需比较 | O(n + maxValue) | O(maxValue) 大小的桶数组 |