博采众长:桶排序
一句话总结
桶排序算法的核心思想分三步:
1、将待排序数组中的元素使用映射函数分配到若干个「桶」中。
2、对每个桶中的元素进行排序。
3、最后将这些排好序的桶进行合并,得到排序结果。
打开下面的可视化面板,多次点击 buckets[index].push(num)
这行代码,即可看到元素分配到不同桶中的过程;多次点击 insertSort(curBucket)
这一行代码,即可看到对每个桶进行排序的过程;多次点击 nums[index++] = num
这行代码,即可看到合并有序桶的过程。
桶排序算法可能并不常见,但我个人感觉它的思想非常有意思,因为你可以在它的算法思想中同时看到前面讲的 归并排序 和 计数排序 的影子,而且。
如果你按顺序学习了前面的所有算法,就会感慨这些算法之间千丝万缕的联系。看看这一代代计算机大佬,就为了「排序」这一个需求,真所谓八仙过海各显神通,精妙的想法层出不穷,我们作为后辈,何不好好品味一下呢?
桶排序的关键点
言归正传,桶排序的思路真的很简单,就是先把待排序数组中的元素分配到若干个桶中,对每个桶中的元素分别进行排序,最后再把这些桶中的元素按顺序合并起来。
这个思路是不是有点像 归并排序?都是把大的数组分成小的数组进行排序,最后再合并起来。不过桶排序更加灵活,三个核心步骤中每一步都可以变化:
1、如何将待排序元素分配到桶中?你需要决定桶的数量,并提供一个映射函数。
2、如何对每个桶中的元素进行排序?理论上可以使用任意排序算法,或者模拟 归并排序 的思路,对每个桶递归地运行桶排序。
3、如何将排好序的桶合并起来?后面的章节会讲 合并多个有序链表/数组 的通用算法,但那个算法会用到 二叉堆结构,且复杂度为 ,这里显然不适用:
如果我都用上二叉堆了,还搞什么桶排序,直接上 堆排序 不就完事了,是吧?所以这一步合并操作的时间复杂度不能超过 ,要做到这一点,就要合理设计分配元素的映射函数。
关于这三个问题,我想首先探讨其中第二个问题。不知道你有没有想过,为什么要把待排序数组分成若干个桶,然后再对每个桶进行排序?这样排序,和直接对整个待排序数组排序相比,真的有区别吗?
答案是,如果暂时不考虑合并有序桶的算法复杂度,那么分开排序当然要比整体排序效率高。
分开排序 vs 整体排序
以最简单的 选择排序 为例,如果我直接对大小为 的数组进行选择排序,那么时间复杂度是 。
假设我们将待排序数组分成 个桶,对于每个桶使用选择排序,那么总的时间复杂度是大于 ,还是小于 ?
这其实是一个简单的数学题,假设有个正整数 ,且它可以分解为 ,那么 和 哪个更大?
有多种思路可以得到答案,我们来看这种几何的思路:
把这个 想象成一个正方形的面积,而 是这个大正方形的一条边上的若干小正方形的面积之和,这样就能直观的理解了,显然这些小正方形的面积没有整个正方形的面积大,所以 。
由此可知,分开排序的时间复杂度总和是小于整体排序的,这就是保证桶排序算法的数学基础。
基于正方形面积的这个抽象,我们还可以更进一步。当 无限大, 无限小时会怎样?
正方形那条边上的小正方形面值之和越来越小,最终会和那条边融为一体,也就是说 的值会无限接近 。
以此观之,如果桶排序将待排序元素分配到尽可能多的桶中( 尽可能大),即每个桶至多只有一个元素时,桶排序就转化成了 计数排序,其复杂度也将降低到 。
即便不能做到每个桶只有一个元素,只要 ,桶排序的时间复杂度也会小于 , 越大,时间复杂度越接近 。
反过来,如果取最小值 ,那桶排序就完全退化成了选择排序,时间复杂度是 。
当然,上述分析都没有考虑合并有序桶的时间复杂度,不过只要能在 的时间内进行合并,那么桶排序的总时间复杂度依然是小于 的。
下面我将来探讨如何把待排序元素分配到桶中,以及如何合并有序桶,最后给出桶排序的几种代码实现。