排序——基数排序(Radix sort)

本文深入解析基数排序(Radixsort)的原理与实现,包括LSD和MSD两种方法,通过图解和代码示例,详细阐述了基数排序的算法思路、性能分析及代码实现。

概述

基数排序(Radix sort)属于“分配式排序”(distribution sort),它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用。

基数排序方法有两类:按照位数比较顺序的不同将其分为 LSD(Least significant digital) MSD(Most significant digital)两类。

  1. 最高位优先(Most Significant Digit First)法,简称MSD法:先按 k1 排序分组,同一组中记录,关键码 k1 相等,再对各组按 k2 排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码 kd 对各子组排序后。再将各组连接起来,便得到一个有序序列。
  2. 最低位优先(Last Significant Digit First)法,简称 LSD 法:先从 kd 开始排序,再对 kd-1 进行排序,依次重复,直到对 k1 排序后便得到一个有序序列。

下图我们针对 LSD 法进行讲解。

算法思路

基数排序的思想就是先排好个位,然后排好个位的基础上排十位,以此类推,直到遍历最高位次,排序结束。基数排序不是比较排序,而是通过分配和收集的过程来实现排序。初始化 10 个固定的桶,桶下标为 0-9。

1、将所有待比较数据统一为相同位数长度;

2、按照个位数进行排序;

3、按照十位数进行排序;

4、以此类推,已知到最高位进行排序。这样就得到一个有序序列。

图解算法

假设有一个数列 {53, 542, 3, 63, 14, 214, 154, 748, 616} 进行排序的过程如下图所示。

动画展示

算法性能

时间复杂度

初看起来,基数排序的执行效率似乎好的让人无法相信,所有要做的只是把原始数据项从数组复制到链表,然后再复制回去。如果有 10 个数据项,则有 20 次复制,对每一位重复一次这个过程。假设对 5 位的数字排序,就需要 20*5=100 次复制。如果有100 个数据项,那么就有 200*5=1000 次复制。复制的次数与数据项的个数成正比,即O(n)。这是我们看到的效率最高的排序算法。

不幸的是,数据项越多,就需要更长的关键字,如果数据项增加 10  倍,那么关键字必须增加一位(多一轮排序)。复制的次数和数据项的个数与关键字长度成正比,可以认为关键字长度是 N 的对数。

平均、最好、最坏都为O(k*n),其中 k 为常数,n 为元素个数。

空间复杂度

O(n + k)

稳定性

稳定

代码实现

C和C++

/*
*求数据的最大位数,决定排序次数
*/
int maxbit(int data[], int n) {
    int d = 1; //保存最大的位数
    int p = 10;
    for(int i = 0; i < n; ++i) {
        while(data[i] >= p) {
            p *= 10;
            ++d;
        }
    }
    return d;
}

void radixSort(int data[], int n) {
    int d = maxbit(data, n);
    int tmp[n];
    int count[10]; //计数器
    int i, j, k;
    int radix = 1;
    for(i = 1; i <= d; i++)  {
        //进行d次排序
        for(j = 0; j < 10; j++) {
            count[j] = 0; //每次分配前清空计数器
        }
        for(j = 0; j < n; j++) {
            k = (data[j] / radix) % 10; //统计每个桶中的记录数
            count[k]++;
        }
        for(j = 1; j < 10; j++) {
            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
        }
        for(j = n - 1; j >= 0; j--) {
            //将所有桶中记录依次收集到tmp中
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for(j = 0; j < n; j++) {
            //将临时数组的内容复制到data中
            data[j] = tmp[j];
        }
        radix = radix * 10;
    }
}

Java

/**
 * 基数排序
 * 考虑负数的情况还可以参考: https://code.i-harness.com/zh-CN/q/e98fa9
 */
public class RadixSort implements IArraySort {

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        int maxDigit = getMaxDigit(arr);
        return radixSort(arr, maxDigit);
    }

    /**
     * 获取最高位数
     */
    private int getMaxDigit(int[] arr) {
        int maxValue = getMaxValue(arr);
        return getNumLenght(maxValue);
    }

    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }

    protected int getNumLenght(long num) {
        if (num == 0) {
            return 1;
        }
        int lenght = 0;
        for (long temp = num; temp != 0; temp /= 10) {
            lenght++;
        }
        return lenght;
    }

    private int[] radixSort(int[] arr, int maxDigit) {
        int mod = 10;
        int dev = 1;

        for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
            // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
            int[][] counter = new int[mod * 2][0];

            for (int j = 0; j < arr.length; j++) {
                int bucket = ((arr[j] % mod) / dev) + mod;
                counter[bucket] = arrayAppend(counter[bucket], arr[j]);
            }

            int pos = 0;
            for (int[] bucket : counter) {
                for (int value : bucket) {
                    arr[pos++] = value;
                }
            }
        }

        return arr;
    }

    /**
     * 自动扩容,并保存数据
     *
     * @param arr
     * @param value
     */
    private int[] arrayAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }
}

 

 

### 计数排序基数排序的时间复杂度分析 #### 计数排序时间复杂度 计数排序适用于整数值的排序,其核心在于通过统计各个不同键值的数量来实现排序。此方法不需要比较元素之间的大小关系,而是利用了数组索引来记录频率。因此,在理想情况下,即输入数据范围已知且有限时,计数排序能够达到线性的平均情况性能。 具体来说,假设`n`代表待排序列表中的元素数量,而`k`则指代这些元素可能取到的最大值加一(因为是从0开始计算)。那么整个过程涉及遍历原始序列一次用于填充辅助数组以及再次扫描这个辅助结构以重建有序的结果集。所以总的操作次数大致等于两倍于原集合长度加上最大值域宽,最终得出计数排序的时间复杂度为\( O(n + k) \)[^2]。 #### 基数排序时间复杂度 基数排序则是另一种非对比型排序技术,它按照数字的不同位来进行分组处理,通常采用稳定版的子排序方式如桶排序或上述提到过的计数排序完成每一轮次的任务。由于每位上的分布独立对待,故整体耗时取决于两个因素:一是参与运算的对象总数目`n`;二是所考虑的关键字宽度或者说最高有效位的位置`d`。当针对固定精度的数据类型执行此类操作时,比如十进制下的电话号码串,实际运行效率接近于线性级别\[O(d * n)\][^1]。 值得注意的是,这里的`d`实际上反映了被排序项内部结构特征——例如对于32比特无符号整形而言大约相当于log₂(2³²)=32轮迭代;而对于日常生活中常见的身份证号之类的字符串形式,则更贴近字符数目本身。如果限定条件允许将`d`视为常量级参数的话,那就可以简化表述成近似线性的\( O(n) \)特性。 #### 性能差异及适用场景 两者都属于高效能的选择之一,但在应用场景上有所区别: - **计数排序**更适合用来解决那些具有较小离散区间特性的简单对象排列问题; - **基数排序**则广泛应用于多字段组合而成的大容量记录集中,特别是面对较长但规律性强的信息编码体系时表现出色。 综上所述,两种算法各有千秋,选择哪一种应视具体情况而定。 ```python def counting_sort(arr, exp=1): output = [0] * len(arr) count = [0] * 10 for i in range(len(arr)): index = (arr[i]//exp)%10 count[index] += 1 for i in range(1, 10): count[i] += count[i - 1] i = len(arr)-1 while i>=0: index = (arr[i]//exp)%10 output[count[index]-1]=arr[i] count[index]-=1 i-=1 for i in range(len(arr)): arr[i] = output[i] def radix_sort(arr): max_num = max(arr) exp = 1 while max_num/exp > 0: counting_sort(arr, exp) exp *= 10 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

努力的老周

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

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

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

打赏作者

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

抵扣说明:

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

余额充值