【算法】基数排序

基数排序
  • 算法描述
    非比较排序,「基」指的是数的位,例如十进制数123,共有百十个位,共3个位。基数排序 按数字的位进行循环,每一***作都是对当前位(基数)的计数排序,使得输出到arr后所有数字在截止到当前位上(即去掉未考察的位后)是排序状态,考察完最大位后完成排序。具体过程如下:

遍历待排序数组arr,找到最大值,计算其位数,例如arr中最大数为123,则maxDigitLen = 3。
数组的数字为n进制,就创建大小为n的计数数组countArr,也可以称为n个桶。
开始「位」的for循环,循环次数等于maxDigitLen,每一轮对 当前所有数字的当前位 执行一次 计数排序。
每次计数排序结束后将结果写回arr。
for循环结束后返回排序结果arr。
如下动图演示{6674, 1560, 5884, 2977, 2922, 4127, 5390, 7870, 1193, 7163}的基数排序过程。

也可以不使用计数排序,而是创建一个二维数组(可看作19个桶)保存每次遍历的中间结果,此写法耗费的空间较大(每个桶的大小都要等于arr大小+1,空间复杂度为O(19n)),是稳定排序,不详细说明,可以参考后续给出的代码实现。

以计数排序为基础的基数排序,每一位循环时都对所有数做该位的计数排序。

不以计数排序为基础的基数排序,每一位循环时都将所有数按顺序放入相应的桶中。

  • 稳定性:取决于计数排序的稳定性。

两个同值的元素位数相同,按位循环,若采用稳定版本的计数排序则稳定,否则不稳定。

  • 优化
    处理负数优化:若存在负数,可以先找到最小值(负数),对arr中的每个数,都加上此最小值的绝对值,排序完成后再减回去。但加法可能使得 数字越界,一种更好的办法是计数排序时 将countArr的大小扩展为 19,以[0, 19]对应可能出现的[-9, 9]。因此在每轮求当前基数时,要在原基数结果上 +9 以对应countArr的下标。

后续代码给出利用计数排序的应用此优化的版本。

  • 复杂度分析
    时间复杂度:d为绝对值最大的元素位数,总共进行d轮计数排序,O(n + k)O(n+k) 是计数排序的复杂度,其中k是位的取值范围,如果是非负数,则 k = 10 (0~9),如果包含负数,则 k = 19 (-9~9)。所以总的时间复杂度为 O(d(n + k))O(d(n+k))。

  • 空间复杂度:

利用计数排序的基数排序,空间复杂度与计数排序相同,为 O(n + k)O(n+k)。

不利用计数排序的计数排序,将以19个(应用处理负数优化)「桶」的二维数组作为排序过程中的存储空间,故空间复杂度为 O(19n)O(19n),当n显著大于19时也可认为其空间复杂度为 O(n)O(n)。
代码

  • 以计数排序为基础
public int[] radixSort(int[] arr) {
    if (arr.length < 2) return arr;
    int max = Math.abs(arr[0]); // 找到arr中绝对值最大者
    for (int i = 1; i < arr.length; i++) {
        max = Math.max(max, Math.abs(arr[i]));
    }
    int maxDigitLen = 0, base = 10; // 最大位数 & 基(几进制就是几)
    while (max != 0) {
        maxDigitLen++;
        max /= base;
    }
    // 在接下来的for中,每一轮都对当前位(基数)执行一次计数排序
    int[] sortedArr = new int[arr.length];
    for (int i = 0; i < maxDigitLen; i++) {
        int[] countArr = new int[19]; // 处理负数优化
        // 根据每一个数字当前位的数字,累计相应位置的计数
        for (int j = 0; j < arr.length; j++) {
            // 此步处理要注意,当base大于10时,例如base=100时,1234%100=34
            // 还需要再除以(base/10),得到的3,然后再+9(考虑负数)才是本次的bucketIdx
            int bucketIdx = (arr[j] % base) / (base / 10) + 9;
            countArr[bucketIdx]++;
        }
        // countArr变形,得到每个下标所代表的arr中的数的当前位在arr中的最大位置(从1开始)
        for (int j = 1; j < countArr.length; j++) {
            countArr[j] += countArr[j - 1];
        }
        // 逆序输出保持稳定性
        for (int j = arr.length - 1; j >= 0; j--) {
            int thisBase = (arr[j] % base) / (base / 10) + 9;
            // countArr[thisBase]得到的从1开始计算的位置,转成下标要-1
            sortedArr[countArr[thisBase] - 1] = arr[j]; 
            countArr[thisBase]--;
        }
        // 完成当前位的计数排序后将排序结果拷贝回原数组
        arr = Arrays.copyOf(sortedArr, sortedArr.length);
        // base进一位,准备下一轮对下一位的计数排序
        base *= 10;
    }
    return arr;
}

不以计数排序为基础

public int[] radixSort(int[] arr) {
    if (arr.length < 2) return arr;
    // 找到arr中绝对值最大者
    int max = Math.abs(arr[0]);
    for (int i = 1; i < arr.length; i++) {
        max = Math.max(max, Math.abs(arr[i]));
    }
    int maxDigitLen = 0, base = 10; // 最大位数 & 基
    while (max != 0) {
        maxDigitLen++;
        max /= base;
    }
    // arr.length + 1的作用是令每个桶的第0位保存该桶的元素个数。
    int[][] buckets = new int[19][arr.length + 1]; // 处理负数优化
    // 在每一位上将数组中所有具有该位的数字装入对应桶中
    for (int i = 0; i < maxDigitLen; i++) {
        for (int j = 0; j < arr.length; j++) {
            // 此步处理要注意,当base大于10时,例如base=100时,1234%100=34
            // 还需要再除以(base/10),得到的3才是本次的bucketIndex
            int bucketIdx = (arr[j] % base) / (base / 10) + 9; // +9使其可以处理负数
            int currentBucketQuantity = buckets[bucketIdx][0];
            buckets[bucketIdx][currentBucketQuantity + 1] = arr[j];
            buckets[bucketIdx][0]++;
        }
        // 将当前所有桶的数按桶序,桶内按低到高输出为本轮排序结果
        int arrIdx = 0;
        for (int j = 0; j < buckets.length; j++) {
            for (int k = 1; k <= buckets[j][0]; k++) {
                arr[arrIdx++] = buckets[j][k];
            }
        }
        // 每一轮过后将桶计数归零
        for (int[] bucket : buckets) bucket[0] = 0;
        base *= 10; // 调整base
    }
    return arr;
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值