美文网首页
常用非比较类排序算法

常用非比较类排序算法

作者: 王三的猫阿德 | 来源:发表于2017-09-28 17:39 被阅读53次

    前言

    本篇完全转载于常用排序算法总结(二)

    其中有部分代码进行了更改,更改成java语言。

    非对比类排序算法

    非比较排序算法:计数排序,基数排序,桶排序。在一定条件下,它们的时间复杂度可以达到O(n)。

    计数排序

    计数排序用到一个额外的计数数组C,根据数组C来将原数组A中的元素排到正确的位置。

    通俗地理解,例如有10个年龄不同的人,假如统计出有8个人的年龄不比小明大(即小于等于小明的年龄,这里也包括了小明),那么小明的年龄就排在第8位,通过这种思想可以确定每个人的位置,也就排好了序。当然,年龄一样时需要特殊处理(保证稳定性):通过反向填充目标数组,填充完毕后将对应的数字统计递减,可以确保计数排序的稳定性。

    计数排序的步骤如下:

    • 统计数组A中每个值A[i]出现的次数,存入C[A[i]]
    • 从前向后,使数组C中的每个值等于其与前一项相加,这样数组C[A[i]]就变成了代表数组A中小于等于A[i]的元素个数
    • 反向填充目标数组temp:将数组元素A[i]放在数组B的第C[A[i]]个位置(下标为C[A[i]] - 1),每放一个元素就将C[A[i]]递减
    // 分类 ------------ 内部非比较排序
    // 数据结构 --------- 数组
    // 最差时间复杂度 ---- O(n + k)
    // 最优时间复杂度 ---- O(n + k)
    // 平均时间复杂度 ---- O(n + k)
    // 所需辅助空间 ------ O(n + k)
    // 稳定性 ----------- 稳定
    public static void countingSort(int A[], int n) {
        int[] C = new int[99];
        int[] temp = new int[n];
    
        for (int i : A) {
            C[i]++;
        }
    
        for (int i = 1; i < C.length; i++) {
            C[i] = C[i] + C[i - 1];
        }
    
        for (int i = n - 1; i >= 0; i--) {
            temp[--C[A[i]]] = A[i];
        }
    
        for (int i = 0; i < temp.length; i++) {
            A[i] = temp[i];
        }
    }
    

    计数排序的时间复杂度和空间复杂度与数组A的数据范围(A中元素的最大值与最小值的差加上1)有关,因此对于数据范围很大的数组,计数排序需要大量时间和内存。

    例如:对0到99之间的数字进行排序,计数排序是最好的算法,然而计数排序并不适合按字母顺序排序人名,将计数排序用在基数排序算法中,能够更有效的排序数据范围很大的数组。

    单纯的计数排序,只适用于非负数数列排序,而且temp数组的大小与A中元素的最大值和最小值差有关系,不确定性因素很强。

    基数排序

    基数排序将所有待比较正整数统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始进行基数为10的计数排序,一直到最高位计数排序完后,数列就变成一个有序序列(利用了计数排序的稳定性)。

    // 分类 ------------- 内部非比较排序
    // 数据结构 ---------- 数组
    // 最差时间复杂度 ---- O(n * dn)
    // 最优时间复杂度 ---- O(n * dn)
    // 平均时间复杂度 ---- O(n * dn)
    // 所需辅助空间 ------ O(n * dn)
    // 稳定性 ----------- 稳定
    public static void lsdRadixSort(int arr[], int n) {
        int dn = 3; // 假设最高是三位数排序
        for (int d = 1; d <= dn; d++) {
            countingSort(arr, n, d);
        }
    }
    
    // 计数排序
    public static void countingSort(int arr[], int n, int d) {
        int[] C = new int[10]; // 0-9共十个数
        int[] temp = new int[n];
    
        for (int i : arr) {
            C[getDigit(i, d)]++;
        }
    
        for (int i = 1; i < C.length; i++) {
            C[i] = C[i] + C[i - 1];
        }
    
        for (int i = n - 1; i >= 0; i--) {
            int digit = getDigit(arr[i], d);
            temp[--C[digit]] = arr[i];
        }
    
        for (int i = 0; i < temp.length; i++) {
            arr[i] = temp[i];
        }
    }
    
    // 获取第n位数
    public static int getDigit(int x, int d) {
        int[] radix = {1, 1, 10, 100};
        return (x/radix[d]) % 10;
    }
    

    下图给出了对{ 329, 457, 657, 839, 436, 720, 355 }进行基数排序的简单演示过程

    基数排序.jpg

    基数排序的时间复杂度是O(n * dn),其中n是待排序元素个数,dn是数字位数。这个时间复杂度不一定优于O(nlogn),dn的大小取决于数字位的选择(比如比特位数),和待排序数据所属数据类型的全集的大小;dn决定了进行多少轮处理,而n是每轮处理的操作数目。

    如果考虑和比较排序进行对照,基数排序的形式复杂度虽然不一定更小,但由于不进行比较,因此其基本操作的代价较小,而且如果适当的选择基数,dn一般不大于log n,所以基数排序一般要快过基于比较的排序,比如快速排序。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序并不是只能用于整数排序。

    桶排序

    桶排序也叫箱排序。工作的原理是将数组元素映射到有限数量个桶里,利用计数排序可以定位桶的边界,每个桶再各自进行桶内排序(使用其它排序算法或以递归方式继续使用桶排序)。

    // 分类 ------------- 内部非比较排序
    // 数据结构 --------- 数组
    // 最差时间复杂度 ---- O(nlogn)或O(n^2),只有一个桶,取决于桶内排序方式
    // 最优时间复杂度 ---- O(n),每个元素占一个桶
    // 平均时间复杂度 ---- O(n),保证各个桶内元素个数均匀即可
    // 所需辅助空间 ------ O(n + bn)
    // 稳定性 ----------- 稳定
    
    /* 本程序用数组模拟桶 */
    const int bn = 5;    // 这里排序[0,49]的元素,使用5个桶就够了,也可以根据输入动态确定桶的数量
    int C[bn];           // 计数数组,存放桶的边界信息
    
    void InsertionSort(int A[], int left, int right)
    {
        for (int i = left + 1; i <= right; i++)  // 从第二张牌开始抓,直到最后一张牌
        {
            int get = A[i];
            int j = i - 1;
            while (j >= left && A[j] > get)
            {
                A[j + 1] = A[j];
                j--;
            }
            A[j + 1] = get;
        }
    }
    
    int MapToBucket(int x)
    {
        return x / 10;    // 映射函数f(x),作用相当于快排中的Partition,把大量数据分割成基本有序的数据块
    }
    
    void CountingSort(int A[], int n)
    {
        for (int i = 0; i < bn; i++)
        {
            C[i] = 0;
        }
        for (int i = 0; i < n; i++)     // 使C[i]保存着i号桶中元素的个数
        {
            C[MapToBucket(A[i])]++;
        }
        for (int i = 1; i < bn; i++)    // 定位桶边界:初始时,C[i]-1为i号桶最后一个元素的位置
        {
            C[i] = C[i] + C[i - 1];
        }
        int *B = (int *)malloc((n) * sizeof(int));
        for (int i = n - 1; i >= 0; i--)// 从后向前扫描保证计数排序的稳定性(重复元素相对次序不变)
        {
            int b = MapToBucket(A[i]);  // 元素A[i]位于b号桶
            B[--C[b]] = A[i];           // 把每个元素A[i]放到它在输出数组B中的正确位置上
                                        // 桶的边界被更新:C[b]为b号桶第一个元素的位置
        }
        for (int i = 0; i < n; i++)
        {
            A[i] = B[i];
        }
        free(B);
    }
    
    void BucketSort(int A[], int n)
    {
        CountingSort(A, n);          // 利用计数排序确定各个桶的边界(分桶)
        for (int i = 0; i < bn; i++) // 对每一个桶中的元素应用插入排序
        {
            int left = C[i];         // C[i]为i号桶第一个元素的位置
            int right = (i == bn - 1 ? n - 1 : C[i + 1] - 1);// C[i+1]-1为i号桶最后一个元素的位置
            if (left < right)        // 对元素个数大于1的桶进行桶内插入排序
                InsertionSort(A, left, right);
        }
    }
    

    下图给出了对{ 29, 25, 3, 49, 9, 37, 21, 43 }进行桶排序的简单演示过程

    桶排序.jpg

    桶排序不是比较排序,不受到O(nlogn)下限的影响,它是鸽巢排序的一种归纳结果,当所要排序的数组值分散均匀的时候,桶排序拥有线性的时间复杂度。

    计数排序是根据统计量来进行排序,不进行比较,适用于更广的场景。

    基数排序是对计数排序的优化,因为计数排序的空间负责度取决于数列中最大数值和最小数值的差,如果相差非常大,耗费空间就非常多。

    桶排序也可以视为对计数排序的优化,只要桶的间隔选择的好,时间复杂度能得到很大的优化。

    相关文章

      网友评论

          本文标题:常用非比较类排序算法

          本文链接:https://www.haomeiwen.com/subject/mpdvextx.html