算法 - 桶排序

前面分析了几种常用排序算法的原理、时间复杂度、空间复杂度、稳定性等。接下来我会讲三种时间复杂度是 O(n) 的排序算法:桶排序、计数排序、基数排序。


因为这些排序算法的时间复杂度是线性的,所以我们把这类排序算法叫作线性排序(Linear sort)。之所以能做到线性的时间复杂度,主要原因是,这三个算法是非基于比较的排序算法,都不涉及元素之间的比较操作。这几种排序算法理解起来都不难,时间、空间复杂度分析起来也很简单,但是对要排序的数据要求很苛刻,所以我们今天学习重点的是掌握这些排序算法的适用场景。


按照惯例,我先给你出一道思考题:如何根据年龄给 100 万用户排序? 你可能会说,我用上一节课讲的归并、快排就可以搞定啊!是的,它们也可以完成功能,但是时间复杂度最低也是 O(nlogn)。有没有更快的排序方法呢?让我们一起进入今天的内容!


桶排序(Bucket sort)首先,我们来看桶排序。桶排序,顾名思义,会用到“桶”,核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。(有木有点像归并排序)


987564607b864255f81686829503abae.jpg


桶排序的时间复杂度为什么是 O(n) 呢?


我们一块儿来分析一下。如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(nlog(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。


桶排序看起来很优秀,那它是不是可以替代我们之前讲的排序算法呢?答案当然是否定的。为了让你轻松理解桶排序的核心思想,我刚才做了很多假设。


实际上,桶排序对要排序数据的要求是非常苛刻的。*
首先,要排序的数据需要很容易就能划分成 m 个桶,并且,桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序。其次,数据在各个桶之间的分布是比较均匀的。如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,很不平均,那桶内数据排序的时间复杂度就不是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。


桶排序比较适合用在外部排序**中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。比如说我们有 10GB 的订单数据,我们希望按订单金额(假设金额都是正整数)进行排序,但是我们的内存有限,只有几百 MB,没办法一次性把 10GB 的数据都加载到内存中。这个时候该怎么办呢?


现在我来讲一下,如何借助桶排序的处理思想来解决这个问题。我们可以先扫描一遍文件,看订单金额所处的数据范围。假设经过扫描之后我们得到,订单金额最小是 1 元,最大是 10 万元。我们将所有订单根据金额划分到 100 个桶里,第一个桶我们存储金额在 1 元到 1000 元之内的订单,第二桶存储金额在 1001 元到 2000 元之内的订单,以此类推。每一个桶对应一个文件,并且按照金额范围的大小顺序编号命名(00,01,02…99)。理想的情况下,如果订单金额在 1 到 10 万之间均匀分布,那订单会被均匀划分到 100 个文件中,每个小文件中存储大约 100MB 的订单数据,我们就可以将这 100 个小文件依次放到内存中,用快排来排序。等所有文件都排好序之后,我们只需要按照文件编号,从小到大依次读取每个小文件中的订单数据,并将其写入到一个文件中,那这个文件中存储的就是按照金额从小到大排序的订单数据了。


不过,你可能也发现了,订单按照金额在 1 元到 10 万元之间并不一定是均匀分布的 ,所以 10GB 订单数据是无法均匀地被划分到 100 个文件中的。有可能某个金额区间的数据特别多,划分之后对应的文件就会很大,没法一次性读入内存。这又该怎么办呢?针对这些划分之后还是比较大的文件,我们可以继续划分,比如,订单金额在 1 元到 1000 元之间的比较多,我们就将这个区间继续划分为 10 个小区间,1 元到 100 元,101 元到 200 元,201 元到 300 元…901 元到 1000 元。如果划分之后,101 元到 200 元之间的订单还是太多,无法一次性读入内存,那就继续再划分,直到所有的文件都能读入内存为止。




桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中


同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

1. 什么时候最快

当输入的数据可以均匀的分配到每一个桶中。

2. 什么时候最慢

当输入的数据被分配到了同一个桶中。

元素分布在桶中
Bucket_sort_1.svg_.png
然后,元素在每个桶中排序:
Bucket_sort_2.svg_.png


代码

排序一个数组[5,3,6,1,2,7,5,10], 值都在1-10之间,建立10个桶:


[0 0 0 0 0 0 0 0 0 0] 桶,十个空桶


[1 2 3 4 5 6 7 8 9 10] 桶代表的值
遍历数组,第一个数字5,第五个桶加1

1
[0 0 0 0 1 0 0 0 0 0]

第二个数字3,第三个桶加1

1
[0 0 1 0 1 0 0 0 0 0]

遍历后

1
[1 1 1 0 2 1 1 0 0 1]

输出

1
[1 2 3 5 5 6 7 10]

代码:
**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def bucket_sort(lst):
"""创建桶"""
buckets = [0] * ((max(lst) - min(lst))+1)
for i in range(len(lst)):
"""对应下标添加标识位"""
buckets[lst[i]-min(lst)] += 1
res=[]
for i in range(len(buckets)):
if buckets[i] != 0:
res += [i+min(lst)]*buckets[i]
print(res)


def main():
array = [5,3,6,2,7,5,10,11,20]
bucket_sort(array)


if __name__ == '__main__':
main()