算法入门(十二):桶排序

191 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第22天,点击查看活动详情

之前所有的排序其实都和比较有关,都是一种基于比较的排序,而桶排序不是。

image.png

计数排序

我们来看一个例子,例如给你一个arr,已经告诉你这个是员工年龄了,让你去排序,这个时候我们可以根据实际情况 - 年龄区间一般在0-200且都为正整数,来生成一个新的数组,长度为201,然后我们遍历原数组,例如遍历到33,那么我们就给新数组index为33的地方++....完成遍历之后就得到了频次表了,然后平铺开就称完成了排序,复杂度为O(N)

image.png

上述这种方式如果比较的数字范围很大其实就不行了,必须考虑实际数据状况,所以这种基于数据状况进行的排序没有基于比较(你告诉怎么比大小)的排序通用性强,需要定制化比较高且应用场景固定,但是针对具体场景时间复杂度一般比较好

基数排序

基数排序的总体思路是首先判断出最大数字的位数,例如最大数字为100,那么最大位是百位,然后给所有数字补上缺失位数:

image.png

接着对于十进制数字来说,单一位上总共分为0-9个数字,我们配置10个桶对应上去(下面只是画不下了):

image.png

然后针对个位数字,十位数字,百位数字按照队列顺序放到对应桶子里面去,然后从左往右依次倒出来:

image.png

第一轮保证个位顺序排好了,第二轮是十位:

image.png

第三轮是百位数字:

image.png

其实就是按照优先级进行个位,十位,百位进行排序,高位数字晚排序,优先级高

上面这个方法比计数排序要好,要通用一些。只要是涉及进制的数字都可以排序。另外如果有负数可以考虑两个解决方案,首先是遍历求位数时顺带找到最小值(负数),所有数减去最小值,然后得到结果后再加回去就行了,或者新增一位代表符号,也进行排序!

但是如果数据状况没有进制就不行了。

代码实现与讲解

// 针对非负值

public static void radixSort(int[] arr){

    if(arr == null || arr.length < 2){

        return;

    }

    radixSort(arr,0,arr.length - 1, maxbits(arr));

}



public static int maxbits(int[] arr){

    // 遍历找到最大值然后求位数

    int max = Interger.MIN_VALUE;

    for(int i = 0; i < arr.length; i++){

        max = Math.max(max, arr[i]);

    }

    int res = 0;

    while(max != 0){

        res++;

        max /= 10;

    }

    return res;

}



// arr[L..R]排序

public static void radixSort(int[] arr, int L, int R, int digit){

    final int radix = 10;

    int i = 0,j = 0;

    // 有多少个数字就准备多少辅助空间

    int[] bucket = new int[R-L+1];

    for(int d = 1; d < digit; d++){ // 有多少位就进出多少次

        // 10个空间

        // count[0] 当前位(d位)是0的数字有多少个

        // count[1] 当前位是0和1的数字有多少个

        // count[2] 当前位是0,1,2的数字有多少

        ...

        int[] count = new int[radix]; // count[0...9]

        // 取出对应位置数字 - count为词频结果

        for(i = L; i <= R; i++){

            j = getDigit(arr[i],d);

            count[j]++;

        }

        // 处理成前缀和  

        for(i = 1;i <radix;i++){

            count[i] = count[i - 1] + count[i];

        }

        for(i = R; i >= L; i--){ // 数组从右到左遍历 - 入bucket

            j = getDigit(arr[i],d);

            bucket[count[j] - 1] = arr[i];

            count[j]--;

        }

        for(i = L,j=0;i <= R; i++){ // 出桶更新原数组

            arr[i] = bucket[j];

        }

    }

}

这里重点分析一下radixSort内部入桶和出桶的过程,这里面代码实现的逻辑稍微有点绕。

首先我们准备一个count数组,作为0-9号桶记录对应位数的词频,但是注意记录的不是下标出现的次数,而是需要处理成前缀和的形式,这样方便后续出桶排序时候进行分片:

image.png

看上图:原先下标2的含义是2数字有多少个,现在是数字≤2的由多少个。有4个

接着我们声明一个和原先arr等长的help数组,然后就是确定位置了,分析顺序是原数组从右往左(先进先出):062这个数字,第一轮是针对个位,个位数字是2,对应前缀和为4,意思即为小于等于2的数字有4个,而062是最先出来的,所以应该放到第4个数字位置,即4-1=3位置,help[3]即为062,同时前缀和-- count[2] = 3,后续同理:

image.png

最后结束,基于count数组完成了分片,相当于完成了入桶和出桶过程!

image.png