算法学习:前端优雅的实现桶排序(基数排序)及排序算法总结

194 阅读3分钟

基数排序

  • 在常见排序算法中,如冒泡,选中,插入,希尔,归并(详情:juejin.cn/post/703107… ),堆排,快排(详情: juejin.cn/post/703145…) 都是基于比较的排序,在决定两个数前后位置的时候,都是需要把两个数进行大小比较来决定。
  • 基数排序是一种非比较型 整数 排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较
  • 基数排序的应用范围有限,基数排序对有负数和0的数列难以进行排序,基于比较的排序更为通用。
  • 基数排序的时间复杂度可以做到O(N),即有限次数循环可以完成排序。

桶排序(基数排序的一种)实现思路

数组:[51, 102, 15, 12, 13, 1325, 6, 23, 45],用基数排序对其排序:

步骤:

  1. 遍历数组,获取值最大的数,得到它的进制位数,如最大值1325 一共 4位(个,十,百,千)

  2. 按照进制的长度定义桶的大小,如:十进制的桶长度即为10, bucket = Array(10)

  3. 外循环最大的进位,如循环4次,依次对每个数个位,十位,百位,千位···进行出入桶的操作

  4. 循环数组,对数组每项求得当前进位数,存入桶中统计词频。如: 个位数的词频统计

    bucket个位数词频
    key:索引0123456789
    value:词频0122031000
  5. 循环桶,对词频求前缀和 bucket[i] = bucket[i -1] + bucket[i]。bucket[4] = 8,表示,有8个数组项的个位 <=4。

    bucket个位数词频
    key:索引0123456789
    value:前缀求和0135899999
  6. 逆序遍历数组,将arr中的每一项加入到辅助数组(存储arr按照个位数排序的结果)中。先求得arr[i]的个位数X,通过bucket[X]可以获得按照个位数排序的,arr[i]应该所在的位置。随即加入到help数组中,help[bucket[X] - 1] = arr[i],然后要将词频和减1,即bucket[X]--。

  7. 拷贝辅助数组到原数组中,至此,arr已经完成个位的排序。后面依次循环十位,百位,千位···,最后完成整个数组的排序。

这里的第6步有点绕,详细说明一下:

  • 首先在内循环第一步统计词频的时候,是正序循环arr的。
  • 内循环第二步得到词频前缀求和的结果,这里举例上面的数组,如数组项23个位数是3(原来数组最后一个个位数是3的项),桶中的前缀和bucket[3] = 5, 表示按照个位数排序的结果([51, 102, 12, 13, 23, 15, 1325, 35, 6]),23应为第五项,前面有5个项的个位数<= 3。
  • 内循环第三步,一定要逆序遍历arr,因为统计的是的时候是正序遍历的,后统计的项应先取出,维持对之前排序结果的结果的不改变。

代码如下:

function bucketSort(arr, decimal = 10) {

    // 根据传入的进位digit,获取num上的指定进制的值,如:
    //  (1325, 1) => 5 取个位数,
    //  (1325, 3) => 3取百分位数
    function getDigitNum(num, digit) {
        let value = 0;
        while (digit) {
            value = num % 10;
            num = num / 10 | 0;
            digit--;
        }
        return value;
    }

    // 获取数组中最大的数,它的进制位数,如1325 一共 4位
    function getMaxDigitLength(arr) {
        let max = Number.MIN_SAFE_INTEGER;

        let index = 0;
        while (index < arr.length) {
            if (max < arr[index]) {
                max = arr[index];
            }
            index++;
        }
        let maxDigitLength = 0;
        while (max) {
            max = max / 10 | 0;
            maxDigitLength++;
        }
        return maxDigitLength
    }

    const maxDigitLength = getMaxDigitLength(arr)
    
    let digit = 1;
    // 按照进制的长度定义桶,十进制的桶长度即为10
    let bucket = Array(decimal).fill(0);
    // 辅助数组,长度同原始数组长度
    let help = Array(arr.length);
    let index = 0
    // 循环最大的进位,依次对每个数个位,十位,百位,千位···进行出入桶的操作
    for (; digit <= maxDigitLength; digit++) {
        bucket = bucket.fill(0);
        index = 0;
        // 循环数组,对数组每项求得当前进位数,存入桶中统计词频。如:
        // digit = 2时,即arr每项数字十分位上进行词频统计,最终得到的数组中,如:
        // bucket[1] = 9表示,有9个数组项的十分位是1。
        for (; index < arr.length; index++) {
            bucket[getDigitNum(arr[index], digit)]++;
        }

        // 对桶中的词频进行前缀求和,最终得到的数组中。
        // 如digit = 2时,bucket[4] = 14,表示,有14个数组项的十分位 <=4。
        index = 1;
        for (; index < bucket.length; index++) {
            bucket[index] = bucket[index - 1] + bucket[index];
        }

        // 逆序遍历数组,将arr中的每一项加入到辅助数组中
        index = arr.length - 1;
        for (; index >= 0; index--) {
            help[--bucket[getDigitNum(arr[index], digit)]] = arr[index];
        }

        // 遍历辅助数组,赋值给arr。至此本轮的排序结束,如digit = 2时,arr每项的十分位至此已经按照大小顺序排好
        index = 0;
        for (; index < arr.length; index++) {
            arr[index] = help[index];
        }
    }
}

排序算法总结

基于比较的算法总结:

排序算法时间复杂度空间复杂度稳定性
选择排序O(N²)O(1)不能
冒泡排序O(N²)O(1)
插入排序O(N²)O(1)
归并排序O(N * logN)O(N)不能
堆排序O(N * logN)O(1)
快速排序O(N * logN)O(logN)
  • 稳定性的定义是:若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
  • 基于比较的排序算法中,目前没有时间复杂度维持在O(N * logN)级,空间复杂度维持O(1),还能保持稳定性
  • 对于大数据量的排序,快排的效率最高,因为在快排的时间复杂度常数项最小。如果小数据量,如60以下的排序,插入排序的效率反而更高,所以我们可以优化快排(递归过程判断小数据量的时候,使用插入排序),可以达到提高速度的目的。
  • 系统实现的Array.sort实现,v8 引擎 sort 排序策略是在数组长度小于 10 时使用 InsertionSort(插入排序),在大于 10 时使用 In-place QuickSort(原地分区版的快速排序,即使用较少的空间实现的快速排序)www.dazhuanlan.com/mingrong/to…