5.计数排序、基数排序、桶排序

计数排序、基数排序、桶排序

桶思想,适用范围:量大但取值范围小。比如:如何快速得知高考名次?、某大型企业数万名员工年龄排序。

非比较排序。

计数排序,基数排序,桶排序等非比较排序算法,平均时间复杂度都是O(n)。这些排序因为其待排序元素本身就含有了定位特征,因而不需要比较就可以确定其前后位置,从而可以突破比较排序算法时间复杂度O(nlgn)的理论下限。

1、计数排序(桶思想排序用的最多的一种)

非比较排序,桶思想的一种。

是一种时间复杂度为O(n)的排序算法,其思路是开一个长度为len = maxValue-minValue+1 的数组,然后分配空间。扫描一遍原始数组,以(当前值- minValue)作为这个数组的下标。然后扫描原数组,发现对应的一个值将该下标的计数器增1。这一步称为收集。
举个例子, nums=[2, 1, 3, 1, 5] , 首先扫描一遍获取最小值和最大值,
maxValue=5 , minValue=1 ,于是开一个长度为5的计数器数组 counter ,

  1. 分配。统计每个元素出现的频率,得到 counter=[2, 1, 1, 0, 1] ,例如 counter[0] 表示值 0+minValue=1 出现了2次。
  2. 收集。 counter[0]=2 表示 1 出现了两次,那就向原始数组写入两个1, counter[1]=1 表示 2 出现了1次,那就向原始数组写入一个2,依次类推,最终原始数组变为 [1,1,2,3,5] ,排序好了。

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

计数排序算法思想:

  • 第一步分配空间:我们需要确定待排序数组中的取值范围,根据这个取值范围来创建一个长度为(len = maxValue-minValue+1)的用来计数的数组C。这个数组C的下标对应的就是待排序数组的取值范围。下标(key = 待排序数组中的当前值 - minValue)

  • 第二部收集数据:遍历待排序数组,找到一个与计数数组C下标相等的数,就将计数数组C该下标对应的值加1,来统计待排序数组中这个数出现了几次。

  • 最后根据计数数组统计的值,将待排序数组排好序。一一写入。

计数排序算法分析:

  • 时间复杂度:O(n+k),n是待排序数组长度,k是计数数组长度。
  • 空间复杂度:O(n+k),额外数组:长度为k的计数数组,长度和待排序数组一样的临时数组。
  • 稳定,但要注意下面算法是怎样来保证稳定的。

计数排序算法实现:

package book;

/**
 * 计数排序 ,算法的步骤如下:
 *         1、找出待排序的数组中最大和最小的元素
 *         2、统计数组中每个值为t的元素出现的次数,存入数组C的第t项
 *         3、对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加),形成累加数组
 *         4、反向填充目标数组:将每个元素t放在新数组的第C(t)项,每放一个元素就将C(t)减去1
 *
 * 累加数组的作用:是为了计数排序稳定,保证相同数据排好序后的相对次序不变,
 * 原因是:累加数组中的值代表这原待排序数组的数在排好序之后 出现的最后位置,
 *        将原待排序数组从后往前遍历,出现某个数,看这个数在累加数组中记录的位置在那,从这个位置开始往前放,
 *        保证了相同数据排好序后的相对次序
 */
public class CountSort {

    public static void main(String[] args) {
        int[] a = new int[] { 2, 5, 3, 1, 2, 3, 1, 3 };
        int[] b = new int[a.length];

        System.out.println("计数排序前为:");
        print(a);
        System.out.println();

        countSort(a, b, getMaxNumber(a),getMinNumber(a));

        System.out.println("计数排序后为:");
        print(b);
        System.out.println();
    }

    /**
     * @param a 原数组-待排序数组
     * @param b 排好序的数组
     * @param maxValue 待排序数组中取值范围的最大值
     * @param minValue 待排序数组中取值范围的最小值
     */
    public static void countSort(int[] a, int[] b, final int maxValue, final int minValue) {
        //k 代表取值范围,这个案例中 取值范围为0~5,所以计数数组的长度为6,下标0~5。
        //初始化计数数组(这也就是那个桶)
        int[] c = new int[maxValue-minValue + 1];
        //统计待排序数组中,某个数出现了几次,构建计数数组
        for (int i = 0; i < a.length; i++) {
            c[a[i]-minValue]++;
        }
        System.out.println("\n****************");
        System.out.println("计数排序第2步后,计数数组C变为:");
        print(c);

        //构建累加数组
        for (int i = 1; i < c.length; i++) {
            c[i] =  c[i] + c[i - 1];
        }
        System.out.println("\n计数排序第3步后,计数数组C变为累加数组:");
        print(c);

        //从后往前遍历待排序数组
        for (int i = a.length - 1; i >= 0; i--) {
//            b[c[a[i]-minValue] - 1] = a[i];// c[a[i]-minValue]代表元素a[i]的元素个数,c[a[i]]-minValue]-1就是a[i]在b中的位置
//            c[a[i]-minValue]--;

            //或者:
            b[--c[a[i]-minValue]] = a[i];
        }
        System.out.println("\n计数排序第4步后,计数数组C变为:");
        print(c);

        System.out.println("\n计数排序第4步后,数组B变为:");
        print(b);

        System.out.println();
        System.out.println("****************\n");
    }

    public static int getMaxNumber(int[] a) {
        int max = a[0];
        for (int i = 0; i < a.length; i++) {
            if (max < a[i]) {
                max = a[i];
            }
        }
        return max;
    }
    public static int getMinNumber(int[] a) {
        int min = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min > a[i]) {
                min = a[i];
            }
        }
        return min;
    }

    public static void print(int[] keys) {
        for (int key : keys) {
            System.out.print(key+" ");
        }
    }
    
    /**
     * @param a 原数组-待排序数组
     * @param b 排好序的数组
     * @param k 取值范围 maxValue-minValue
     */
    public static void countSort2(int[] a, int[] b, final int k){
        //计数数组,其长度为待排序数组的取值范围+1
        int[] c = new int[k+1];
        //收集数据,
        for (int i = 0; i < a.length; i++) {
            c[a[i]]++;
        }
        System.out.print("计数数组C:");
        print(c);
        System.out.println();

        //累加数组
        for (int i = 1; i < c.length; i++) {
            c[i] = c[i] + c[i-1];
        }
        System.out.print("计数数组C变为累加数组C:");
        print(c);
        System.out.println();

        //将a从后往前遍历  进行排序
        for (int i = a.length-1; i >= 0; i--) {
            b[--c[a[i]]] = a[i];
//            b[c[a[i]] - 1] = a[i];
//            c[a[i]]--;
        }
        System.out.print("计数数组c:");
        print(c);
        System.out.println();

        System.out.print("排序数组b:");
        print(b);
        System.out.println();
    }


}

2、基数排序

非比较排序,桶思想的一种。

本质上是一种多关键字排序。

有低位优先和高位优先两种

  • LSD 、MSD(Least Significant Digit、Most Significant Dight)
  • MSD属于分治的思想

**基数排序(radix sorting)**将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。 然后 从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。具体过程可以。

假设我们有一些二元组(a,b),要对它们进行以a为首要关键字,b的次要关键字的排序。我们可以先把它们先按照首要关键字排序,分成首要关键字相同的若干堆。然后,在按照次要关键值分别对每一堆进行单独排序。最后再把这些堆串连到一起,使首要关键字较小的一堆排在上面。按这种方式的基数排序称为**MSD(Most Significant Dight)排序。第二种方式是从最低有效关键字开始排序,称为LSD(Least Significant Dight)**排序。首先对所有的数据按照次要关键字排序,然后对所有的数据按照首要关键字排序。要注意的是,使用的排序算法必须是稳定的,否则就会取消前一次排序的结果。由于不需要分堆对每堆单独排序,LSD方法往往比MSD简单而开销小。下文介绍的方法全部是基于LSD的。

基数排序的简单描述就是将数字拆分为个位十位百位,每个位依次排序。因为这对算法稳定要求高,所以我们对数位排序用到上一个排序方法计数排序。因为基数排序要经过d (数据长度)次排序, 每次使用计数排序, 计数排序的复杂度为 On), d 相当于常量和N无关,所以基数排序也是 O(n)。基数排序虽然是线性复杂度, 即对n个数字处理了n次,但是每一次代价都比较高, 而且使用计数排序的基数排序不能进行原地排序,需要更多的内存, 并且快速排序可能更好地利用硬件的缓存, 所以比较起来,像快速排序这些原地排序算法更可取**。对于一个位数有限的十进制数,我们可以把它看作一个多元组,从高位到低位关键字重要程度依次递减。*可以使用基数排序对一些位数有限的十进制数排序*。**

例如我们将一个三位数分成,个位,十位,百位三部分。我们要对七个三位数来进行排序,依次对其个位,十位,百位进行排序,

很显然,每一位的数的大小都在[0,9]中,对于每一位的排序用计数排序再适合不过。

基数排序算法实现:

package book;

import java.util.Arrays;

/**
 * 基数排序,里面也用到了计数排序
 */
public class RadixSort {
    public static void main(String[] args) {
        int[] a = {421,420,115,532,305,430,124};

        System.out.println("基数排序前为:");
        print(a);
        System.out.println();

        int[] sort = radixSort(a, getMaxWei(a));
        print(sort);
    }

    /**
     * 基数排序
     * @param keys 待排序数组
     * @param max 最大数最高位-循环的次数
     * @return 排序数组
     */
    public static int[] radixSort(int[] keys,int max){
        int[] b = new int[keys.length];
        //桶
        int[] c = new int[10];

        //这里的3代表着,三位数,要循环3遍,应该写个方法求数组中最大数的最高位
        for (int i = 0; i < max; i++) {
            int division = (int)Math.pow(10,i);
            System.out.println("位数:"+division);
            //初始化计数数组
            for (int j = 0; j < keys.length; j++) {
                int num = keys[j]/division % 10;
                System.out.println("余数:"+num);
                c[num]++;
            }
            System.out.print("计数数组C:");
            print(c);
            System.out.println();

            //累加数组
            for (int j = 1; j < c.length; j++) {
                c[j] = c[j] + c[j-1];
            }
            System.out.print("计数数组C变为累加数组C:");
            print(c);
            System.out.println();

            //将keys数组从后往前遍历  进行排序
            for (int j = keys.length-1; j >= 0; j--) {
                int num = keys[j]/division % 10;
                b[--c[num]] = keys[j];
            }
            //完成一遍之后把此时的 b数组 再复制会待排序数组
            System.arraycopy(b,0,keys,0,keys.length);
            //将计数数组里面的值都变为0,开始下一次的循环计数
            Arrays.fill(c,0);
        }

        return b;
    }

    //求最大数最高位
    public static int getMaxWei(int[] keys) {
        int count = 0;
        int max = keys[0];
        for (int i = 0; i < keys.length; i++) {
            if (max < keys[i]) {
                max = keys[i];
            }
        }

        while (max != 0) {
            max = max/10;
            count++;
        }
        return count;
    }

    public static void print(int[] keys) {
        for (int key : keys) {
            System.out.print(key+" ");
        }
    }
}

3、桶排序

首先定义桶,桶为一个数据容器,每个桶存储一个区间内的数。依然有一个待排序的整数序列A,元素的最小值不小于0,最大值不超过K。假设我们有M个桶,第i个桶Bucket[i]存储i* K/M至(i+1)*K/M之间的数。(计数排序中定义的计数数组里每个位置就是一个桶,基数排序中的所用到的计数数组也是多个桶)

桶排序步骤如下:

  1. 扫描序列A,根据每个元素的值所属的区间,放入指定的桶中(顺序放置)。
  2. 对每个桶中的元素进行排序,什么排序算法都可以,例如:插入排序
  3. 依次收集每个桶中的元素,顺序放置到输出序列中。

桶排序算法分析

  • 时间:求最大最小值n,桶初始化k,遍历装桶n,桶内排序n/k* lg(n/k) * k,结果输出n,3n+k + n/k * lg(n/k) * k=3n+ k + n* lg(n/k)约等于 n+k ,最坏n方(一个桶),最好为n (n个桶而且值排列均匀)
  • 空间:n+k 但实际上空间做到最好的话,就只能用链表,时间就做不到最好

一个案例:

package book;

/**
 *  桶排序
 */
public class BucketSort {
    
    public static void main(String[] args) {
        int[] arr = new int[] {3,5,45,34,2,78,67,34,56,98};
        bucketSort(arr);
    }

    // 插入排序
    static void insertSort(int[] a) {
        int n = a.length;
        for (int i = 1; i < n; i++) {
            int p = a[i];
            insert(a, i, p);
        }
    }

    static void insert(int[] a, int index, int x) {
        // 元素插入数组a[0:index-1]
        int i;
        for (i = index - 1; i >= 0 && x < a[i]; i--) {
            a[i + 1] = a[i];
        }
        a[i + 1] = x;
    }

    private static void bucketSort(int[] a) {
        int M = 10; // 11个桶
        int n = a.length;
        int[] bucketA = new int[M]; // 用于存放每个桶中的元素个数
        // 构造一个二维数组b,用来存放A中的数据,这里的B相当于很多桶,B[i][]代表第i个桶
        int[][] b = new int[M][n];
        int i, j;
        for (i = 0; i < M; i++)
            for (j = 0; j < n; j++)
                b[i][j] = 0;

        int data, bucket;
        for (i = 0; i < n; i++) {
            data = a[i];
            bucket = data / 10;
            b[bucket][bucketA[bucket]] = a[i];// B[0][]中存放A中进行A[i]/10运算后高位为0的数据,同理B[1][]存放高位为1的数据
            bucketA[bucket]++;// 用来计数二维数组中列中数据的个数,也就是桶A[i]中存放数据的个数
        }
        System.out.println("每个桶内元素个数:");
        for (i = 0; i < M; i++) {
            System.out.print(bucketA[i] + " ");
        }
        System.out.println();

        System.out.println("数据插入桶后,桶内未进行排序前的结果为:");
        for (i = 0; i < M; i++) {
            for (j = 0; j < n; j++)
                System.out.print(b[i][j] + " ");
            System.out.println();
        }

        System.out.println("对每个桶进行插入排序,结果为:");
        // 下面使用直接插入排序对这个二维数组进行排序,也就是对每个桶进行排序
        for (i = 0; i < M; i++) {
            // 下面是对具有数据的一列进行直接插入排序,也就是对B[i][]这个桶中的数据进行排序
            if (bucketA[i] != 0) {
                // 插入排序
                for (j = 1; j < bucketA[i]; j++) {
                    int p = b[i][j];
                    int k;
                    for (k = j - 1; k >= 0 && p < b[i][k]; k--)
                    {
                        assert k==-1;
                        b[i][k + 1] = b[i][k];
                    }
                    b[i][k + 1] = p;
                }
            }
        }

        // 输出排序过后的顺序
        for (i = 0; i < 10; i++) {
            if (bucketA[i] != 0) {
                for (j = 0; j < bucketA[i]; j++) {
                    System.out.print(b[i][j] + " ");
                }
            }
        }
    }
}

4、三种排序比较

排序算法时间复杂度空间复杂度
计数排序O(N+K)O(N+K)稳定排序
基数排序O(N)O(N)稳定排序
桶排序O(N+K)O(N+K)稳定排序

从整体上来说,计数排序,桶排序都是非基于比较的排序算法,而其时间复杂度依赖于数据的范围,桶排序还依赖于空间的开销和数据的分布。而基数排序是一种对多元组排序的有效方法,具体实现要用到计数排序或桶排序。

相对于快速排序、堆排序等基于比较的排序算法,计数排序、桶排序和基数排序限制较多,不如快速排序、堆排序等算法灵活性好。但反过来讲,这三种线性排序算法之所以能够达到线性时间,是因为充分利用了待排序数据的特性,如果生硬得使用快速排序、堆排序等算法,就相当于浪费了这些特性,因而达不到更高的效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悬浮海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值