- 计数排序、桶排序、基数排序均为O(n)算法
- 桶排序可以看成是计数排序的升级版,它将要排的数据分到多个有序的桶里,每个桶里的数据再单独排序,再把每个桶的数据依次取出,即可完成排序。
算法原理
桶排序的核心思想就是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶排序完成之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
一句话总结:划分多个范围相同的区间,每个子区间自排序,最后合并。
演示过程


算法关键
- 桶怎么表示?
桶是用来存放数据的,可以用arraylist,linkedlist等【必须动态存放,因为我们不知道每个桶的数据量是多少?】
- 桶需要几个?待排序元素与桶的映射?
【这个要根据元素的特性来决定,原则是:尽可能将元素平均分到每一个桶中】
- 桶内排序用什么方法排?
代码
java
public static void bucketSort(int[] arr){
// 计算最大值与最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
// 计算桶的数量
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
// 将每个元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
// 对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
// 将桶中的元素赋值到原序列
int index = 0;
for(int i = 0; i < bucketArr.size(); i++){
for(int j = 0; j < bucketArr.get(i).size(); j++){
arr[index++] = bucketArr.get(i).get(j);
}
}
}
public static void main(String[] args) {
// double[] arr = new double[]{4.12, 6.421, 0.0023, 3.0, 2.123, 8.122, 4.12, 10.09};
int[] arr = new int[]{11,11,9,21,14,55,77,99,53,25};
bucketSort(arr);
System.out.println(Arrays.toString(arr));
}
C++
#include <algorithm>
#include <unordered_map>
#include <limits>
using namespace std;
vector<int> bucketSort(vector<int>& nums) {
unordered_map<int, int> mapper;
// 计算最大值与最小值
int max = std::numeric_limits<int>::min();
int min = std::numeric_limits<int>::max();
for(auto i : nums){
if(i > max){
max = i;
}
if(i < min){
min = i;
}
}
// 计算桶的数量
int count = (max - min) / nums.size() + 1;
std::vector<std::vector<int>> bucketArr;
bucketArr.resize(count);
// 将每个元素放入桶
for(auto i : nums){
int index = (i - min) / count;
bucketArr[index].emplace_back(i);
}
// 对每个桶进行排序
for (auto & bucket : bucketArr){
sort(bucket.begin(), bucket.end());
}
// 将桶中的元素放入结果中
std::vector<int> ans;
for(auto &bucket : bucketArr){
for (auto &i : bucket){
ans.emplace_back(i);
}
}
return ans;
}
int main(int argc,char **argv){
std::vector<int> test = {11,11,9,21,14,55,77,99,53,25};
auto ans = bucketSort(test);
for(auto &v : ans){
printf("%d\t", v);
}
return 0;
}
go
package main
import (
"fmt"
"sort"
)
func countSort(arr []int) []int {
if len(arr) <= 1 {
return arr
}
// 找最大值和最小值
max := arr[0]
min := arr[0]
for _, v := range arr{
if v < min {
min = v
}
if v > max {
max = v
}
}
// 计算桶的数量并且创建出同
count := (max - min) / len(arr) + 1
buckets := make([][]int, count)
// 将数值分配到各个桶中
for _, v := range arr{
idx := (v - min) / count
if buckets[idx] == nil {
buckets[idx] = make([]int, 0)
}
buckets[idx] = append(buckets[idx], v)
}
// 对每个桶进行排序
for _, bucket := range buckets{
sort.Ints(bucket)
}
// 计数
ans := make([]int, 0)
for _, bucket := range buckets{
for _, value := range bucket{
ans = append(ans, value)
}
}
return ans
}
func main() {
tt := []int{11,11,9,21,14,55,77,99,53,25};
fmt.Println(countSort(tt))
}
复杂度分析
时间复杂度:O(N + C)
- 对于待排序序列大小为 N,共分为 M 个桶,主要步骤有:
- N 次循环,将每个元素装入对应的桶中
- M 次循环,对每个桶中的数据进行排序(平均每个桶有 N/M 个元素)
一般使用较为快速的排序算法,时间复杂度为 O(NlogN),实际的桶排序过程是以链表形式插入的。
整个桶排序的时间复杂度为:

当 N = M 时,复杂度为 O(N)
也就是说:
- 如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用归并排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。
- 所以,桶排序的时间复杂度,取决于对每个桶之间数据进行排序的时间复杂度,桶划分的越小,每个桶之间的数据越小,排序所用的时间也会越少,但是相应的空间小号就会增大。
- 额外空间复杂度:O(N + M)
稳定性分析
桶排序的稳定性取决于桶内排序使用的算法。
-
在额外空间充足的情况下,尽量增大桶的数量,极限情况下每个桶只有一个数据时,或者是每只桶只装一个值时,完全避开了桶内排序的操作,桶排序的最好时间复杂度就能够达到 O(n)。
-
比如高考总分 750 分,全国几百万人,我们只需要创建 751 个桶,循环一遍挨个扔进去,排序速度是毫秒级。
-
但是如果数据经过桶的划分之后,桶与桶的数据分布极不均匀,有些数据非常多,有些数据非常少,比如[ 8,2,9,10,1,23,53,22,12,9000 ]这十个数据,我们分成十个桶装,结果发现第一个桶装了 9 个数据,这是非常影响效率的情况,会使时间复杂度下降到 O(nlogn),解决办法是我们每次桶内排序时判断一下数据量,如果桶里的数据量过大,那么应该在桶里面回调自身再进行一次桶排序。
使用条件与适用场景
桶排序对要排序的数据的要求是十分苛刻的。适用条件如下:
-
首先,要排序的数据需要很容易就能划分为m个桶,并且,桶与桶之间有者天然的大小顺序,这样每个桶内数据都排序完成之后,桶与桶之间的数据不需要再进行排序。
-
其次,数据再各个桶之间的分布比较均匀。如果数据经过桶的划分之后,有些桶的数据非常多,有些非常少,很不平均,那么桶内数据排序的时间复杂度就不是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。
所以,桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
应用案例
需求描述
有10GB的订单数据,需按订单金额(假设金额都是正整数)进行排序,但内存有限,仅几百MB。
解决思路
扫描一遍文件,看订单金额所处数据范围,比如1元-10万元,那么就分100个桶。
第一个桶存储金额1-1000元之内的订单,第二个桶存1001-2000元之内的订单,依次类推。
每个桶对应一个文件,并按照金额范围的大小顺序编号命名(00,01,02,…,99)。
将100个小文件依次放入内存并用快排排序。
所有文件排好序后,只需按照文件编号从小到大依次读取每个小文件并写到大文件中即可。
注意点:若单个文件无法全部载入内存,则针对该文件继续按照前面的思路进行处理即可。
桶排序、计数排序、快速排序对比
桶排序是将待排序集合中处于同一个值领的元素存入同一个桶中,也就是根据元素值特性将集合拆分为多个区域,则拆分后形成的多个桶,从值域上看是处于有序状态的。对每个桶中元素进行排序,则所有桶中元素构成的集合是已经排序的。
- 快速排序是将集合拆分成两个值域,这里成为两个桶,再分别对两个通进行排序,最终完成排序。
- 桶排序则是将集合拆分为多个桶。对每个桶进行排序,则完成排序过程。
两者的不同之处在于:
- 快排是在集合本身上进行排序,属于原地排序方式,而且对每个桶的排序方式也是快排。
- 桶排序是提供了额外的操作空间,在额外空间上对桶进行排序,避免了构成桶过程的元素比较和交换操作,同时可以自主选择恰当的排序算法对桶进行排序。
桶排序更是对计数排序的改进:
- 计数排序申请的额外空间跨度从最小元素值到最大元素值,如果待排序集合中元素不是一次递增的,则必然有空间浪费情况。
- 桶排序则是弱化了这种浪费情况,将最小值到最大值之间的每一个位置申请空间,更新为最小值到最大值之间每一个固定区域申请空间,尽量减少了元素值大小不连续情况下的空间浪费情况

被折叠的 条评论
为什么被折叠?



