基数排序--递归的计数排序

介绍:

RadixSort
另外一种非比较排序算法,基于计数排序(或者说桶排序)进行排序,也可以达到O(n)的复杂度。

思路:

计数排序==>桶排序达到O(n)的情况,要求桶中只有一个元素,或者只有相同的元素。这种如果数据的值跨度过大,或者过于离散,所需要的很多,因为中间的空的数字也会占桶的,导致空间浪费很多。
基数排序很好了解决了这个问题,原始的思路不好推演,我们使用网上其它文章的扑克牌例子:
假设想将扑克牌按照红桃、黑桃、梅花、方块的花色顺序、1~K的点数进行排序。
可以先将牌分成四堆,红桃、黑桃、梅花、方块堆,每一堆里面再按照1~K排序,最终从头红色堆开始拿,就排完序了。
先拆分成大的块,让大块整体有序;逐级递减,继续拆分大块成中快,让中块有序,直到最后剩下单独一位的时候,再排一次序,排序结束。
另外一种想法,不知道是怎么想到的,,,我是想不到,但是确实能实现排序。就是先按最小的排,然后将数组合起来,这样最后一位是有序的。然后按照次高一级排序,这样两级都最小的肯定排在最前的。然后再合并,按照高一级排序,直到最高级,合并数组,完成排序。
从最高级开始排序的,叫做MSD(Most Significant Digit 最大有效数位,就是最高位);从最低级开始排序的,叫做LSD(Least Significant Digit 最低位),两种思路的写法很不相同。

Java代码实现:

// 最高数位,我们再用基数排序的时候,一般这个是已知的,这里我们就直接眼睛看了
private static int topDigit;
private static int resultIndex = 0;
private static int[] result;
public static void main(String[] args) {
    int[] arr = new int[]{22,136,11, 124,235,16,268};
    // 最高数位
    topDigit = 2;
    result = new int[arr.length];
    // 在方法中不太方便组织结果数组,设置结果数组为全局变量
    radixSortMsd(arr, topDigit, arr.length);
    System.out.println(Arrays.toString(result));
    // 重新乱序数组,使用Lsd方式排序
    arr = new int[]{22,136,11, 124,235,16,268};
    // lsd可以直接组织出结果数组
    radixSortLsd(arr, 0);
    System.out.println(Arrays.toString(arr));

    // 重新乱序数组,使用Lsd方式排序
    arr = new int[]{22,136,11, 124,235,16,268};
    radixSortMsdInplace(arr, arr, topDigit, arr.length, 0);
    // lsd可以直接组织出结果数组
    System.out.println(Arrays.toString(arr));
}

/**
 * 从高位排序
 * @param arr
 * @param currentDigit
 * @param length
 */
private static void radixSortMsd(int[] arr, int currentDigit, int length){
    // 桶,只需要10位数的桶
    // 桶是一个二维数组,长度为本次的最长长度
    int[][] buckets = new int[10][length];
    // 使用一个辅助计数数组,用来标记桶中的数组中的有效数据的个数
    int[] bucketsSize = new int[10];

    // 循环arr,取arr[i]当前位的值,作为桶的下标,并计数
    for(int i = 0; i < length; i++){
        int bucketIndex = getCurrentIndexValue(arr[i], currentDigit);
        buckets[bucketIndex][bucketsSize[bucketIndex]] = arr[i];
        // 计数器+1
        bucketsSize[bucketIndex]++;
    }

    // 继续递归,直到位置为0
    for(int i = 0; i< buckets.length;i++){
        int actualSize = bucketsSize[i];
        // 不论何时,只要actualSize == 0,都可以跳过
        if(actualSize <= 0){
            continue;
        }
        // 如果数组中只有一个元素,即使目前仍然在高位,也没必要继续递归下去了
        // 将数据放入新数组,直接进去下一次循环
        int[] bucket = buckets[i];
        if(actualSize == 1){
            result[resultIndex] = bucket[0];
            resultIndex++;
            continue;
        }

        // 如果数组有多个元素,顺序输出这多个元素
        currentDigit--;
        if(currentDigit < 0){
            // 将数组中的元素顺次输出
            for(int j = 0; j < actualSize;j++){
                result[resultIndex] = bucket[j];
                resultIndex++;
            }
            return;
        }
        // 否则进入下一次递归
        radixSortMsd(buckets[i], currentDigit, bucketsSize[i]);
    }
}

/**
 * 从高位排序
 * @param arr
 * @param currentDigit
 * @param length
 * @param startIndex 开始index
 */
private static void radixSortMsdInplace(int[]result, int[] arr, int currentDigit, int length, int startIndex){
    // 桶,只需要10位数的桶
    // 桶是一个二维数组,长度为本次的最长长度
    int[][] buckets = new int[10][length];
    // 使用一个辅助计数数组,用来标记桶中的数组中的有效数据的个数
    int[] bucketsSize = new int[10];

    // 循环arr,取arr[i]当前位的值,作为桶的下标,并计数
    for(int i = 0; i < length; i++){
        int bucketIndex = getCurrentIndexValue(arr[i], currentDigit);
        buckets[bucketIndex][bucketsSize[bucketIndex]] = arr[i];
        // 计数器+1
        bucketsSize[bucketIndex]++;
    }

    // 继续递归,直到位置为0
    for(int i = 0; i< buckets.length;i++){
        int actualSize = bucketsSize[i];
        // 不论何时,只要actualSize == 0,都可以跳过
        if(actualSize <= 0){
            continue;
        }
        // 如果数组中只有一个元素,即使目前仍然在高位,也没必要继续递归下去了
        // 将数据放入新数组,直接进去下一次循环
        int[] bucket = buckets[i];
        if(actualSize == 1){
            result[startIndex] = bucket[0];
            startIndex++;
            continue;
        }

        // 如果数组有多个元素,顺序输出这多个元素
        currentDigit--;
        if(currentDigit < 0){
            // 将数组中的元素顺次输出
            for(int j = 0; j < actualSize;j++){
                result[startIndex] = bucket[j];
                startIndex++;
            }
            return;
        }
        // 否则进入下一次递归
        radixSortMsdInplace(result, buckets[i], currentDigit, bucketsSize[i],startIndex);
        // 这一步很关键,startIndex仍然是上一次递归进去前的startIndex
        // 这一步要将进入递归内的数计算在内
        startIndex += actualSize;
    }
}

/**
 * 获取当前位置的值
 * @param value
 * @param index
 * @return
 */
private static int getCurrentIndexValue(int value, int index){
    String s = String.valueOf(value);
    // 假设value是100,数是3,取0
    // 假设value是100,数是2,取1
    // 如果s的长度比index还小,说明没到这个位数
    if(s.length() - 1 < index){
        return 0;
    }
    // 返回从左边计算的index位置的值
    return Integer.valueOf(String.valueOf(s.charAt(s.length() - 1 - index)));
}

/**
 * 从低位开始排序
 * @param arr
 * @param currentDigit
 */
private static void radixSortLsd(int[] arr, int currentDigit){
    int length = arr.length;
    // 桶,只需要10位数的桶
    // 桶是一个二维数组,长度为本次的最长长度
    int[][] buckets = new int[10][length];
    // 使用一个辅助计数数组,用来标记桶中的数组中的有效数据的个数
    int[] bucketsSize = new int[10];

    // 循环arr,取arr[i]当前位的值,作为桶的下标,并计数
    for(int i = 0; i < length; i++){
        int bucketIndex = getCurrentIndexValue(arr[i], currentDigit);
        buckets[bucketIndex][bucketsSize[bucketIndex]] = arr[i];
        // 计数器+1
        bucketsSize[bucketIndex]++;
    }
    // 将数据合起来
    int newIndex = 0;
    for(int i = 0; i < buckets.length;i++){
        //
        int bucketSize = bucketsSize[i];
        if(bucketSize <= 0){
            continue;
        }
        int[] bucket = buckets[i];
        for(int j = 0; j < bucketSize;j++){
            arr[newIndex] = buckets[i][j];
            newIndex++;
        }
    }

    currentDigit++;
    // 如果已经推进过了最高位
    if(currentDigit > topDigit){
        return;
    }else{
        radixSortLsd(arr, currentDigit);
    }
}


MSD代码解释。。先写的代码(*/ω\*):

由于MSD思路直接,正向,所以我们先看MSD
1、理论上应该找到最高位先,但是这种情况一般数据特征会提前知道,所以我们直接用眼睛看,把高位定下来,当然也可以轮询一遍数组。。。
2、从最高级开始排序,使用桶排序进行排序。
3、遍历所有的桶
1)如果桶是空的,continue
2)如果桶中有一个,即使现在是最高级,也没有必要递归了,这一个分块的排序已经结束,直接输出,或者保存到结果数组中
3)如果桶中有多个,判断当前是否最低级,如果是最低级,可以输出结果,或将结果保存到结果数组中,当前分块排序结束。如果不是最低级,还需要递归进行排序,直到最低数级。
4、需要一个辅助的数组,和原数组长度相同,并且由于是递归,最简单的写法是将结果数组和结果数组的index作为全局变量。
5、另一种写法是,我们不需要辅助数组,就在原数组插入值,当然,桶的辅助数组必不可少。这么考虑,递归的时候,原数组已经没卵用了,已经被拆成一个个块放入桶中了,我们可以直接放入原数组中。
需要将index带着走,每次桶中有数据进入到排序尽头的时候,需要将index加上去。

LSD代码解释:

1、从0开始递归,桶排序,然后合起来。然后数位+1,继续排序,合并,一直到跨国最高位,合并,排序结束
2、没了,这种代码简单,所以很多例子喜欢用这种。。。

复杂度:

基于计数排序,所以是O(n+k),需要桶数组的辅助空间,但是由于被拆成了位数,桶的空间大大降低,并且每次用完都可以释放,不用一次性占用很大的空间。

稳定性:

基于计数排序,具有稳定性。



猜你喜欢

转载自blog.csdn.net/u011531425/article/details/80672323