前置知识

阅读本文前,你需要先学习:

一句话总结

桶排序算法的核心思想分三步:

1、将待排序数组中的元素使用映射函数分配到若干个「桶」中。

2、对每个桶中的元素进行排序。

3、最后将这些排好序的桶进行合并,得到排序结果。

打开下面的可视化面板,多次点击 buckets[index].push(num) 这行代码,即可看到元素分配到不同桶中的过程;多次点击 insertSort(curBucket) 这一行代码,即可看到对每个桶进行排序的过程;多次点击 nums[index++] = num 这行代码,即可看到合并有序桶的过程。

Algorithm visualize
  算法可视化面板

桶排序算法可能并不常见,但我个人感觉它的思想非常有意思,因为你可以在它的算法思想中同时看到前面讲的 归并排序计数排序 的影子。

如果你按顺序学习了前面的所有算法,就会感慨这些算法之间千丝万缕的联系。看看这一代代计算机大佬,就为了「排序」这一个需求,真所谓八仙过海各显神通,精妙的想法层出不穷,我们作为后辈,何不好好品味一下呢?

桶排序的关键点

言归正传,桶排序的思路真的很简单,就是先把待排序数组中的元素分配到若干个桶中,对每个桶中的元素分别进行排序,最后再把这些桶中的元素按顺序合并起来。

这个思路是不是有点像 归并排序?都是把大的数组分成小的数组进行排序,最后再合并起来。不过桶排序更加灵活,三个核心步骤中每一步都可以变化:

1、如何将待排序元素分配到桶中?你需要决定桶的数量,并提供一个映射函数。

2、如何对每个桶中的元素进行排序?理论上可以使用任意排序算法,或者模拟 归并排序 的思路,对每个桶递归地运行桶排序。

3、如何将排好序的桶合并起来?后面的章节会讲 合并多个有序链表/数组 的通用算法,但那个算法会用到 二叉堆结构,且复杂度为 O(nlogk)O(n*logk),这里显然不适用:

如果我都用上二叉堆了,还搞什么桶排序,直接上 堆排序 不就完事了,是吧?所以这一步合并操作的时间复杂度不能超过 O(n)O(n),要做到这一点,就要合理设计分配元素的映射函数。

关于这三个问题,我想首先探讨其中第二个问题。不知道你有没有想过,为什么要把待排序数组分成若干个桶,然后再对每个桶进行排序?这样排序,和直接对整个待排序数组排序相比,真的有区别吗

答案是,如果暂时不考虑合并有序桶的算法复杂度,那么分开排序当然要比整体排序效率高。

分开排序 vs 整体排序

以最简单的 选择排序 为例,如果我直接对大小为 nn 的数组进行选择排序,那么时间复杂度是 O(n2)O(n^2)

假设我们将待排序数组分成 kk 个桶,对于每个桶使用选择排序,那么总的时间复杂度是大于 O(n2)O(n^2),还是小于 O(n2)O(n^2)

这其实是一个简单的数学题,假设有个正整数 nn,且它可以分解为 n=n1+n2++nkn = n_1 + n_2 + \cdots + n_k,那么 n2n^2n12+n22++nk2n_1^2 + n_2^2 + \cdots + n_k^2 哪个更大?

有多种思路可以得到答案,我们来看这种几何的思路:

把这个 n2n^2 想象成一个正方形的面积,而 n12+n22++nk2n_1^2 + n_2^2 + \cdots + n_k^2 是这个大正方形的一条边上的若干小正方形的面积之和,这样就能直观的理解了,显然这些小正方形的面积没有整个正方形的面积大,所以 n2>=n12+n22++nk2n^2 >= n_1^2 + n_2^2 + \cdots + n_k^2

由此可知,分开排序的时间复杂度总和是小于整体排序的,这就是保证桶排序算法的数学基础。

基于正方形面积的这个抽象,我们还可以更进一步。当 kk 无限大,n1,n2,,nkn_1, n_2, \cdots, n_k 无限小时会怎样?

正方形那条边上的小正方形面值之和越来越小,最终会和那条边融为一体,也就是说 n12+n22++nk2n_1^2 + n_2^2 + \cdots + n_k^2 的值会无限接近 nn

以此观之,如果桶排序将待排序元素分配到尽可能多的桶中(kk 尽可能大),即每个桶至多只有一个元素时,桶排序就转化成了 计数排序,其复杂度也将降低到 O(n)O(n)

即便不能做到每个桶只有一个元素,只要 k>1k > 1,桶排序的时间复杂度也会小于 O(n2)O(n^2)kk 越大,时间复杂度越接近 O(n)O(n)

反过来,如果取最小值 k=1k = 1,那桶排序就完全退化成了选择排序,时间复杂度是 O(n2)O(n^2)

当然,上述分析都没有考虑合并有序桶的时间复杂度,不过只要能在 O(n)O(n) 的时间内进行合并,那么桶排序的总时间复杂度依然是小于 O(n2)O(n^2) 的。

下面我将来探讨如何把待排序元素分配到桶中,以及如何合并有序桶,最后给出桶排序的几种代码实现。

桶排序的实现

将元素分配到 k 个桶中

使用插入排序的桶排序

使用递归的桶排序

排序的稳定性

时空复杂度分析

loading...