计数排序和基数排序算法实现Java版

980 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

排序算法是软件开发中非常重要的一环。本期就来讨论关于算法在面试中会遇到的一些问题,其中包含个人一些理解,由于本人学识有限,难免会有纰漏之处,还望读者能多多包涵。

上一期我们讲了快速排序和希尔排序(传送门)。本期继续讲解其他的排序算法。

计数排序

与之前的算法有很大区别,计数排序(Counting sort)不是基于比较的排序算法。它的核心思想在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数,主要是因为需要开一个长度为 maxValue-minValue+1 的数组。

前几期我们讲的算法,时间复杂度不是O(n2)O(n^2)就是O(nlog  n)O(nlog\;n),而计数排序可以将时间复杂度降到O(n)O(n),也就是线性时间。当然这样做了不小的牺牲,即计数排序只能用于整数。所以计数排序的局限性就比较大了。

我们根据其思想来列出编码的步骤:

第一步,求出待排序数组的最大值 max 和最小值 min;第二步,实例化辅助计数数组temp,temp数组中每个下标对应arr中的一个元素,temp用来记录每个元素出现的次数。第三步,计算 arr 中每个元素在temp中的位置 position = arr[i] - min。最后根据 temp 数组求得排序后的数组。

计数排序本质上是一种特殊的桶排序,当桶的个数最大的时候,就是计数排序。

其Java代码实现如下:

public static void main(String[] args) {
    int[] array={1,0,2,8,-3,9};
    countSort(array);
    for(int i=0;i<array.length;i++){
        System.out.print(array[i]+" ");
    }
}
public static void countSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    int min = arr[0];
    int max = arr[0];
    for (int i = 1; i < arr.length; i++) {
        min = Math.min(arr[i], min);
        max = Math.max(arr[i], max);
    }
    int[] countArr = new int[max - min + 1];
    for (int i = 0; i < arr.length; i++) {
        countArr[arr[i] - min]++;
    }
    int index = 0;
    for (int i = 0; i < countArr.length; i++) {
        while (countArr[i]-- > 0) {
            arr[index++] = i + min;
        }
    }
}

从代码中也能看出,计数排序有两个比较致命的缺陷,一个是当数列最大值和最小值之间差距过大时,计数排序将会耗费很大的空间。比如给定20个随机整数,范围在0到1亿之间,此时如果使用计数排序的话,就需要创建长度为1亿的数组,不但严重浪费了空间,而且时间复杂度也随之升高。二个就是当数列元素不是整数时,计数排序将无法使用。例如数列中的元素都是小数,像是3.21、0.68这样子,则无法创建对应的统计数组,这样显然无法进行计数排序。正是由于这两大局限性,才使得计数排序不像快速排序、归并排序那样被人们广泛适用。

基数排序

基数排序是基于数据位数的一种排序算法。它的排序方法比较特别,它是通过关键字数字各位的大小来进行排序。它是一种借助多关键字排序的思想来对单逻辑关键字进行排序的方法。

基数排序的思路就是将每一个数分成一位数一位数的方式进行排序。从小到大的方式比较,基数排序对所有数中的每一位数进行从小到大的分别排序。将权重相同位数上的值分到一组,然后对这进行排序。

然后对下一位权重更高的位数进行排序。这次的排序是基于上一次的排序。如果相等的话,不改变原本的顺序。如果不想等的话会覆盖之前的顺序。

从这个方面可以看出,对每一组进行排序的这个排序算法要求必须是稳定的 。其要保证当值相等的时候,相对位置的顺序不变。(这也算是排序稳定性的一种应用)

如果有的数位数不够的话使用 0 补足。例如:1,234,那么 1 表示为 001。

image.png

它主要有两种排序方法:最高位优先法(MSD):按照关键字位权重高低依此递减来划分子序列;最低位优先法(LSD) :按照关键字位权重低高依此增加来划分子序列。基数排序的思想:分配和回收。

image.png

基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

Java代码如下:

public static void main(String[] args) {
    int[] array={1,0,2,8,9,56,100,74,2589,12};
    RadixSort(array);
    for(int i=0;i<array.length;i++){
        System.out.print(array[i]+" ");
    }
}
//求得数组中的最长位数
public static int MaxDigit (int [] A) {
    if (A == null) {
        return 0;
    }
    int Max = 0;
    for (int i = 0; i < A.length; i++) {
        if (Max < A[i]) {
            Max = A[i];
        }
    }
    int MaxDigit = 0;
    while (Max > 0) {
        MaxDigit++;
        Max /= 10;
    }
    return MaxDigit;
}
public static void RadixSort(int [] A) {  //基数排序
    //创建一个二维数组,类比于在直角坐标系中,进行分配收集操作
    int[][] buckets = new int[10][A.length];
    int MaxDigit = MaxDigit(A);
    //t 用于提取关键字的位数
    int t = 10;
    //按排序趟数进行循环
    for (int i = 0; i < MaxDigit; i++) {
        //在一个桶中存放元素的数量,是buckets 二维数组的y轴
        int[] BucketLen = new int[10];
        //分配操作:将待排数组中的元素依此放入桶中
        for (int j = 0; j < A.length ; j++) {
            //桶的下标值,是buckets 二维数组的x轴
            int BucketIndex = (A[j] % t) / (t / 10);
            buckets[BucketIndex][BucketLen[BucketIndex]] = A[j];
            //该下标下,也就是桶中元素个数随之增加
            BucketLen[BucketIndex]++;
        }
        //收集操作:将已排好序的元素从桶中取出来
        int k = 0;
        for (int x = 0; x < 10; x++) {
            for (int y = 0; y < BucketLen[x]; y++) {
                A[k++] = buckets[x][y];
            }
        }
        t *= 10;
    }
}

上述代码是将基数排序的操作内化在一个二维数组中进行,我们看下和归并排序的对比:

image.png

备注:基数排序中,n 为序列中的关键字数,d为关键字的关键字位数,rd 为关键字位数的个数。

经过我的测试,基数排序虽然时间复杂度是线性的,但实际效果并不好。我用的测试数据的位数是3位,数量是15万个。速度远不如快速排序,所以各位在实际编程时,注意自己的数据规模,再决定使用哪种算法。