排序线性排序

Posted leduoi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序线性排序相关的知识,希望对你有一定的参考价值。

三种时间复杂度为O(n)的排序算法:桶排序、计数排序、基数排序

这三种排序算法都不涉及元素之间的比较操作,也叫做线性排序(Linear sort)

桶排序

核心思想

将要排序的数据分散到有序的桶中,分别对桶中的数据进行排序。排序好了之后,按照桶的顺序依次取出,就得到排好序的数据了。

时间复杂度

时间复杂度为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时,桶排序的时间复杂度就接近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 元之间的订单还是太多,无法一次性读入内存,那就继续再划分,直到所有的文件都能读入内存为止。

计数排序(Counting sort)

核心思想

计数排序是桶排序的一种特殊情况。当要排序的数据所处范围并不大最大值为k时,可以将数据划分到K个桶内。每个桶内的数据值都是相同的,省去了桶内排序时间

  • 求出排序数据的最大范围,根据范围创建出相应个数的桶,并初始化为0
  • 对数据进行遍历,将数据对应的桶内个数加1
  • 根据桶的顺序,依次累加
  • 创建一个和排序一样长度的空数组,再次对数据进行遍历,根据桶中对应的元素个数计算出存入的坐标,存入之后将桶中个数减1
  • 将排好序的数组赋值给原数组
// 计数排序,a是数组,n是数组大小。假设数组中存储的都是非负整数。
public void countingSort(int[] a, int n) {
  if (n <= 1) return;

  // 查找数组中数据的范围
  int max = a[0];
  for (int i = 1; i < n; ++i) {
    if (max < a[i]) {
      max = a[i];
    }
  }

  int[] c = new int[max + 1]; // 申请一个计数数组c,下标大小[0,max]
  for (int i = 0; i <= max; ++i) {
    c[i] = 0;
  }

  // 计算每个元素的个数,放入c中
  for (int i = 0; i < n; ++i) {
    c[a[i]]++;
  }

  // 依次累加
  for (int i = 1; i <= max; ++i) {
    c[i] = c[i-1] + c[i];
  }

  // 临时数组r,存储排序之后的结果
  int[] r = new int[n];
  // 计算排序的关键步骤,有点难理解
  for (int i = n - 1; i >= 0; --i) {
    int index = c[a[i]]-1;
    r[index] = a[i];
    c[a[i]]--;
  }

  // 将结果拷贝给a数组
  for (int i = 0; i < n; ++i) {
    a[i] = r[i];
  }
}

时间复杂度

时间复杂度为O(n)。因为计数排序只涉及到遍历排序数据

要求

  • 只能在数据范围不大的场景应用。如果数据范围要比数据数量大得多,就不适合计数排序
  • 只能给非负整数排序。如果是其他类型,要在不改变数据相对大小情况下,转换为非负整数。如果考生成绩精确到小数后一位,我们就需要将所有的分数都先乘以 10,转化成整数,然后再放到 9010 个桶内。再比如,如果要排序的数据中有负数,数据的范围是[-1000, 1000],那我们就需要先对每个数据都加 1000,转化成非负整数。

思考

如果你所在的省有 50 万考生,如何通过成绩快速排序得出名次呢?

思路:使用计数排序思路。考生的满分是 900 分,最小是 0 分,这个数据的范围很小,所以我们可以分成 901 个桶,对应分数从 0 分到 900 分。根据考生的成绩,我们将这 50 万考生划分到这 901 个桶里。桶内的数据都是分数相同的考生,所以并不需要再进行排序。我们只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现了 50 万考生的排序。因为只涉及扫描遍历操作,所以时间复杂度是 O(n)。

如果考生成绩精确到小数后一位,我们就需要将所有的分数都先乘以 10,转化成整数,然后再放到 9010 个桶内。

基数排序

基数排序就是将数据划分为一位一位的,然后对每一位进行比较排序

基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了

思考

1.假设我们有 10 万个手机号码,希望将这 10 万个手机号码从小到大排序?

分析:使用快速排序时间复杂度为O(nlogn),因为手机号有11位范围太大,显然不适合使用,因此可以使用基数排序

思路:比较两个手机号大小,如果一个手机号前几位比另一个大,那之后就不需要比较了。因此可以借助排序算法的稳定性,先按照最后一位排序手机号,在按照倒数第二位重新排序,以此类推,经过11次排序之后手机号码就有序了。

根据每一位来排序,必须使用稳定排序算法,我们可以用刚讲过的桶排序或者计数排序,它们的时间复杂度可以做到 O(n)。如果要排序的数据有 k 位,那我们就需要 k 次桶排序或者计数排序,总的时间复杂度是 O(k*n)。当 k 不大的时候,比如手机号码排序的例子,k 最大就是 11,所以基数排序的时间复杂度就近似于 O(n)。

2.如何根据年龄给 100 万用户排序?

假设年龄的范围最小 1 岁,最大不超过 120 岁。我们可以遍历这 100 万用户,根据年龄将其划分到这 120 个桶里,然后依次顺序遍历这 120 个桶中的元素。这样就得到了按照年龄排序的 100 万用户数据。

以上是关于排序线性排序的主要内容,如果未能解决你的问题,请参考以下文章

线性排序:桶排序计数排序

线性排序:桶排序计数排序

初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段

初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段

动画展现十大经典排序算法(附代码)

快速排序线性时间?