排序(3)-线性排序

2020-03-18  本文已影响0人  天命_风流

在前面两节中我们介绍了时间复杂度为O(n2)和O(nlogn)的排序算法,它们都是基于比价的排序算法。在这一节我们介绍时间复杂度为O(n)的非基于比较的排序算法:桶排序、计数排序、基数排序。非基于比较,是指算法内部不涉及(较少涉及)元素之间的比较操作。
这种算法的时间复杂度比较低,但是对数据有额外的要求。所以我们还需要学习这些排序算法的适用场景。

桶排序(Bucket sort)

桶排序需要用到“桶”,核心思想是将数据分配到有序的桶中,再在桶内进行排序。桶内排序完成后,再把每个桶里的数据按照顺序取出,它们组成的序列就是有序的了。


桶排序.jpg

桶排序的分析:

性能:

应用条件

桶排序看起来很优秀,但是却无法代替之前我们说过的排序算法,因为桶排序对要排序的数据的要求非常苛刻:

有时候我们会遇到这种情况:排序数据的数据量非常庞大,内存有限,以至于无法将所有数据完全装入内存。面临这样的问题,我们必须借助外部磁盘完成存储排序,这种排序方式就叫外部排序

计数排序(Counting sort)

当排序数据所处的范围不大的时候,我们可以使用计数排序进行排序。计数排序的思想和桶排序的思想大致相同,你甚至可以认为计数排序是桶排序的一种特殊情况
当 n 个数据所处的范围不大,比如最大值为 k 的时候,我们可以将数据分成 k 个桶,每个桶内的数据都是相同的,这样省去了桶内排序的时间。

你会发现,数值为 x 的桶内放入了所有值为 x 的数据,如果你对这些数据进行计数,就可以很容易地得到数值为 x 的数据的个数。是不是觉得这种思想和计数器很像呢?没错,这就是计数排序中计数的由来。

这里我们举个例子:假设有 8 名考生参加考试,考试分数为 0~5,我们将这八名考生的成绩放在一个数组 A[8] 中,他们分别是 : 2,5,3,0,2,3,0,3。
使用数组 C[6] 进行计数,其中下标对应分数,数值对应考生的个数,我们通过一遍遍历就很容易得到 C[6] :

计数排序中的c数组.jpg
如果我们使用 R[8] 作为排序后的数组,C[3] 在 R 中的含义可以表示如下: 计数排序的排序数组.jpg

OK,现在我们得到了计数,那么如何对数据进行排序呢?(如何构造一个完整正确的 R[8] 数组呢?)处理的方法非常巧妙,你需要多看几遍:

首先我们将 C[6] 数组编程累加数组,此时 C[k] 的数据是 小于等于 k 分的考生的个数

计数排序中的c_累加数组.jpg 你可以将这个图和 R[8] 结合着看(注意,此时我们还没有构造 R 数组,但是你要直到累加 C[6] 数组在 R 数组中的含义)。

现在,我们从后往前扫描数组A。例如,当扫描到 3 时,查询 C[3]=7(这代表当前扫描到的 3 排名第七),我们对这个 -1 ,就可以得到这个数据排序后的位置,6(数组从 0 开始计数),所以我们将 R[6] 设置为 3 ,随后对计数数组 C[3] 进行 -1 操作。
以此类推,我们从后往前扫描完成 数组A ,同时我们构造完成了排序后的数组 R :

计数排序的排序构造.jpg

下面是老师给出的代码:

// 计数排序,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];
  }
}

计数排序的分析

性能

应用场景

基数排序(Radix sort)

先思考一个应用场景:请对100万个手机号码进行排序。
分析:手机号码有11位,由于这样的数据范围,显然难以使用桶排序和计数排序完成,这时我们就需要使用基数排序完成。
为了方便,我们举个例子:398 和 752 比较,他们的最高位为 3 和 7 ,仅仅用最高位的比较我们就可以得到两个数的大小。记住这个性质,当待排序的数据具有这个特点的时候我们就可以考虑使用基数排序。

注意,我们从最高位往最低位进行比较,上面的性质是数学性质。但是编码过程不能从前往后,你可以尝试一下,你会发现其中的错误。综上,我们需要从后往前进行比较。

还记得在介绍排序算法中的稳定性的意义时的购物单排序的例子吗?沿用相同的思路,我们对电话从最后开始排序,然后对倒数第二位排序,以此类推,直到第一位排序完成。下面给一个字符串排序的例子:

基数排序.jpg
注意:这里按位排序的算法需要是稳定的,否则排序顺序仍然是混乱的。

基数排序的分析

性能

应用场景

总结

我们学习了三种线性时间复杂度的排序算法:桶排序、计数排序、基数排序。他们对于数据有比较苛刻的要求,所以应用范围有限。

桶排序和计数排序的思想很像,都是针对范围不大的数据。将数据划分成为不同范围的桶来实现。
基数排序要求数据可以划分高低位,同时高低位之间要有递进关系。在基数排序中,我们需要借助桶排序或者计数排序来完成 位的排序工作。


以上就是线性排序算法的所有内容。

注:本文章的主要内容来自我对极客时间app的《数据结构与算法之美》专栏的总结,我大量引用了其中的代码和截图,如果想要了解具体内容,可以前往极客时间

上一篇下一篇

猜你喜欢

热点阅读