线性排序算法和排序算法优化

三种线性排序算法和排序算法优化的笔记

桶排序

计数排序

计数排序只能用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了。
而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

  • 如果存在负数,则先将全部数据增加最小值的绝对值,转化为正整数,最后再加最小值。
  • 如果存在小数,则现将全部数据乘以小数位数,转化为整数,最后除以小数位数。

    示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    public static void countingSort(int[] nums) {
    if (nums == null || nums.length <= 1) {
    return;
    }
    // 解决负数
    int min = 0;
    for (int i = 1; i < nums.length; i++) {
    if (min > nums[i]) {
    min = nums[i];
    }
    }
    if (min < 0) {
    for (int i = 0; i < nums.length; i++) {
    nums[i] = nums[i] + Math.abs(min);
    }
    }
    // 解决小数 略...
    // 查找数组中数据的范围
    int max = nums[0];
    for (int i = 1; i < nums.length; i++) {
    if (max < nums[i]) {
    max = nums[i];
    }
    }
    // 创建计数数组
    int[] counter = new int[max + 1];
    for (int i = 0; i < counter.length; i++) {
    counter[i] = 0;
    }
    // 计算每个元素的个数,放入计数数组中
    for (int i = 0; i < nums.length; i++) {
    counter[nums[i]]++;
    }
    // 依次累加
    for (int i = 1; i <= max; i++) {
    counter[i] = counter[i - 1] + counter[i];
    }
    // 创建临时数组,存储排序之后的结果
    int[] temp = new int[nums.length];
    // 计算排序步骤,从后向前遍历
    for (int i = nums.length - 1; i >= 0; i--) {
    int index = counter[nums[i]] - 1;
    temp[index] = nums[i];
    counter[nums[i]]--;
    }
    // 复制临时数组到原数组
    for (int i = 0; i < nums.length; i++) {
    int t = temp[i];
    if (min < 0) {
    t = t + min;
    }
    nums[i] = t;
    }

    }

基数排序

假设有10万个手机号码,将这10万个手机号码从小到大排序。

这个问题里有这样的规律:假设要比较两个手机号码a,b的大小,如果在前面几位中,a手机号码已经比b手机号码大了,那后面的几位就不用看了。 借助稳定排序算法,这里有一个巧妙的实现思路。先按照最后一位来排序手机号码,然后,再按照倒数第二位重新排序,以此类推,最后按照第一位重新排序。经过11次排序之后,手机号码就都有序了。

注意,这里按照每位来排序的排序算法要是稳定的,否则这个实现思路就是不正确的。因为如果是非稳定排序算法,那最后一次排序只会考虑最高位的大小顺序,完全不管其他位的大小关系,那么低位的排序就完全没有意义了。

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

排序算法优化

各类常见排序算法复杂度比较

02

如果对小规模数据进行排序,可以选择时间复杂度是O(n2)的算法;
如果对大规模数据进行排序,时间复杂度是O(nlogn)的算法更加高效。
所以,为了兼顾任意规 模数据的排序,一般都会首选时间复杂度是O(nlogn)的排序算法来实现排序函数。

如何优化快速排序?

快速排序的时间复杂度是O(n2),如果数据原来就是有序的或者接近有序的,每次分区点都选择最后一个数据,
快速排序算法就会变得非常糟糕,时间复杂度就会退化为O(n2)。实际上,这种O(n2)时间复杂度出现的主要原因还是因为我们分区点选的不够合理。

如何选择分区点

最理想的分区点是:被分区点分开的两个分区中,数据的数量差不多。

三数取中法

从区间的首、尾、中间,分别取出一个数,然后对比大小,取这3个数的中间值作为分区点。这样每间隔某个固定的长度,取数据出来比较,将中间值作为分区点的分区算法,肯定要比单纯取某一个数据更好。但是,如果要排序的数组比较大,那“三数取中”可能就不够了,可能要“五数取中”或者“十数取中”。

随机法

随机法就是每次从要排序的区间中,随机选择一个元素作为分区点。这种方法并不能保证每次分区点都选的比较好,但是从概率的角度来看,也不大可能会出现每次分区点都选的很差的情况,所以平均情况下,这样选的分区点是比较好的。时间复杂度退化为最糟糕的O(n2)的情况,出现的可能性不大。

C语言的qsort实现

qsort()会优先使用归并排序来排序输入数据,因为归并排序的空间复杂度是O(n),所以对于小数据量的排序,比如1KB、2KB等, 归并排序只需要额外1KB、2KB的内存空间。

要排序的数据量比较大的时候,qsort()会改为用 快速排序算法来排序。同时qsort()选择分区点的方法是“三数取中法”。

由于递归太深可能会导致堆栈溢出问题,qsort()是通过自己实现一个堆上的栈,手动模拟递归来解决的。

在快速排序的过程中,当要排序的区间中,元素的个数小于等于4时,qsort()就退化为插入排序。

投食入口