内容简介:桶排序、计数排序和基数排序这三种算法的时间复杂度都为 ,因此,它们也被叫作线性排序(Linear Sort)。之所以能做到线性,是因为这三个算法是桶排序看起来很优秀,但事实上,桶排序对排序数据的要求是非常苛刻的。假如我们有 10 GB 的订单数据需要按照金额进行排序,但内存只有几百 MB ,这时候该怎么办呢?
桶排序、计数排序和基数 排序 这三种算法的时间复杂度都为 ,因此,它们也被叫作线性排序(Linear Sort)。之所以能做到线性,是因为这三个算法是 非基于比较 的排序算法,都不涉及元素之间的比较操作。
1. 桶排序(Bucket Sort)?
1.1. 桶排序原理
- 桶排序 ,顾名思义,要用到“桶”。核心思想是将要排序的数据分到几个有序的桶里,每个桶的数据再单独进行排序。桶内排完序后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
1.2. 桶排序的时间复杂度分析
- 如果要排序的数据有 个,我们把它们均匀地划分到 个桶内,每个桶内就有 个元素。对每个桶内的数据进行快速排序,时间复杂度为 。 个桶排序时间复杂度就为 。当桶的个数接近数据个数时, 就是一个非常小的数,这个时候通排序的时间复杂度接近于 。
1.3. 桶排序的适用条件
桶排序看起来很优秀,但事实上,桶排序对排序数据的要求是非常苛刻的。
- 首先, 要排序的数据需要很容易就能划分为 个桶,并且桶与桶之间有着天然的大小顺序 。
- 其次, 数据在各个桶之间的分布是比较均匀的 。
- 桶排序比较适合用在 外部排序 中。所谓的外部排序就是数据存储在外部磁盘中,数据比较大而内存有限,无法将数据全部加载到内存中去。
1.4. 一个桶排序的实例
假如我们有 10 GB 的订单数据需要按照金额进行排序,但内存只有几百 MB ,这时候该怎么办呢?
-
我们先扫描一遍文件,确定订单金额的数据范围。
-
如果扫描后发现订单金额处于 1 万元到 10 万元之间,我们将所有订单按照金额划分到 100 个桶内,第一个桶数据范围为[1, 1000],第二个桶数据范围为[1001, 2000]......,每个桶对应一个文件,同时将文件按照金额范围的大小顺序编号命名(如00、01、02...99)。
-
如果订单金额分布均匀,则每个文件包含大约 100 MB 的数据,我们可以将每个小文件读入到内存中,进行快速排序。然后,再按顺序从各个小文件读取数据,写入到另外一个文件,即是排序好的数据了。
-
如果订单分布不均,某一范围内数据特别多无法一次读入内存,则可以继续对此区间再进行划分,直到所有的文件都可以读入内存为止。
2. 计数排序(Counting Sort)?
2.1. 计数 排序算法 实现
-
计数排序可以看作是桶排序的一种特殊情况。当要排序的数据所处的范围并不大时,比如最大值为 ,这时候,我们可以把数据分为 个桶,每个桶内的数据都是相同的,省掉了桶内排序的时间。
-
假设高考分数的范围为 [0, 750],我们可以将考生的分数划分到 751 个桶内,然后再依次从桶内取出数据,就可以实现对考生成绩的排序了。因为只涉及到扫描遍历操作,因此时间复杂度为 。
但这个排序算法为什么叫计数排序呢,这是由计数排序算法的实现方法来决定的,我们来看一个简单的例子。
-
假设有 8 个考生,他们的分数范围为 [0, 5]。这 8 个考生的成绩我们放在一个数组中,A[8] = {2, 5, 3, 0, 2, 3, 0, 3}。
-
我们用大小为 6 的数组代表 6 个桶来统计考生的成绩分布情况,其中 下标表示考生的分数,数组内的值表示这个分数的考生个数 。我们遍历一遍数组后,就可以得到 C[6] = {2, 0, 2, 3, 0, 1,},得 0 分的共有 2 人,得 3 分的共有 3 人。
-
如下所示,成绩为 3 的考生共有 3 个,小于 3 分的考生共有 4 个,所以在排好序的数据 R[8] 中,3 的位置应该为 4,5 和 6 。
- 而我们怎么得到这个位置呢?只需要对 C[6] 数组按顺序累计求和即可,这时候,C[6] 数组中的每个值就都表示 小于等于它的值的个数 了。
-
接下来,我们从后到前依次扫描数组 A[8]。当扫描到 3 时,我们取出 C[3] 的值 7,说明小于等于 3 的个数为 7 个,那么 3 就应该放在数组 R[8] 的第 7 个位置,也就是下标为 6 的地方。当我们再次遇到 3 的时候,这时候小于等于 3 的元素个数就少了一个,也就是我们要把 C[3] 相应地减去 1 。
-
之所以要从后到前依次扫描数组,是因为这样 之前相同的元素就仍然会保持相同的顺序,可以保证排序算法的稳定性 。
-
当我们扫描完整个数组 A[8] 时,数组 R[8] 中的数据也就从小到大排好序了,其详细过程可参考下图。
- 代码实现
// 假设数组中存储的都是非负整数 void Counting_Sort(int data[], int n) { if (n <= 1) { return; } // 寻找数组的最大值 int max = data[0]; for (int i = 1; i < n; i++) { if (data[i] > max) { max = data[i]; } } // 定义一个计数数组, 统计每个元素的个数 int c[max+1] = {0}; for (int i = 0; i < n; i++) { c[data[i]]++; } // 对计数数组累计求和 for (int i = 1; i <= max; i++) { c[i] = c[i] + c[i-1]; } // 临时存放排好序的数据 int r[n] = {0}; // 倒序遍历数组,将元素放入正确的位置 for (int i = n-1; i >= 0; i--) { r[c[data[i]] - 1] = data[i]; c[data[i]]--; } for (int i = 0; i < n; i++) { data[i] = r[i]; } } 复制代码
2.2. 计数排序的适用范围
-
计数排序只适用于 数据范围不大的场景中 ,如果数据范围 比排序的数据 大很多,就不适合用计数排序了。
-
计数排序能给 非负整数排序 ,如果数据是其他类型的,需要将其在不改变相对大小的情况下,转化为非负整数。比如数据有一位小数,我们需要将数据都乘以 10;数据范围为 [-1000, 1000],我们需要对每个数据加 1000。
3. 基数排序(Radix Sort)?
假设要对 10 万个手机号码进行排序,显然桶排序和计数排序都不太适合,那怎样才能做到时间复杂度为 呢?
1.1. 基数排序原理
-
手机号码有这样的规律,假设要比较两个手机号码 的大小,如果在前面几位中, 手机号码已经比 大了,那后面几位就不用看了。
-
借助 稳定排序算法 ,我们可以这么实现。从手机号码的最后一位开始,分别按照每一位的数字对手机号码进行排序,依次往前进行,经过 11 次排序之后,手机号码就都有序了。
-
下面是一个字符串的排序实例,和手机号码类似。
-
根据每一位的排序,我们可以用刚才的桶排序或者计数排序来实现,它们的时间复杂度可以做到 。如果排序的数据有 位,则总的时间复杂度为 ,当 不大时,基数排序的时间复杂度就近似为 。
-
有时候,要排序的数据并不都是等长的,比如我们要对英文单词进行排序。这时候,我们可以 把所有单词都补足到相同长度,位数不够的在后面补 ’0‘ ,所有字母的 ASCII 码都大于 ‘0’,因此不会影响原有的大小顺序。
-
基数排序需要数据可以分割出独立的位出来,而且位之间有递进的关系。除此之外,每一位的数据范围都不能太大,要可以用线性排序算法来进行排序。
获取更多精彩,请关注「seniusen」!
以上所述就是小编给大家介绍的《排序算法下——桶排序、计数排序和基数排序》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 计数排序vs基数排序vs桶排序
- 基于桶的基数排序
- 小白学数据结构——四、排序算法Python(桶、计数、基数排序)
- Elasticsearch 高基数聚合性能提升 3 倍,改动了什么?
- 图形化排序算法比较:快速排序、插入排序、选择排序、冒泡排序
- 算法之常见排序算法-冒泡排序、归并排序、快速排序
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。