线性排序---桶&计数&基数


(极客时间专栏的总结)

桶排序 (线性)

非基于比较的算法, 都不涉及元素之间的比价操作.

桶排序的核心思想是: 将要排序的数据分到几个有序的桶里, 每个桶里的数据再单独进行排序. 桶内排完序之后, 再把每个桶里的数据按照顺序依次取出, 组成的序列就是有序的了.

如果要排序的数据有 n 个, 我们把他们均匀地划分到 m 个桶内, 每个桶里就有 k = n/m 个数据. 每个桶内部使用快排, 时间复杂度 O(klogk), m 个桶排序的时间复杂度就是 O(m*klogk) = O(nlogn/m), m 非常接近 n 时, O(n)

桶排序对数据的要求是非常苛刻的

首先, 要排序的数据需要很容易就能划分成 m 个桶, 并且, 桶与桶之间有着天然的大小排序. 这样, 每个桶内的数据排完序之后, 桶与桶之间的数据就不需要再进行排序了.

其次, 数据在各个桶之间的分布要求是比较均匀的. 否则在极端情况下, 所有数据都放到一个桶中, 排序会退化成快排 O(nlogn)

应用场景:

桶排序比较适合用在外部排序中, 外部排序就是数据存储在外部磁盘中, 数据量比较大, 内存有限, 无法将数据全部加载到内存中.

扫描二维码关注公众号,回复: 5567992 查看本文章

比如对 10GB 的订单按照金额进行排序, 可以先扫描一遍文件, 将一定范围的金额放到一个桶内, 一个桶对应一个文件. 排好序后, 只需要按照文件编号, 从小到大依次将每个订单写入到一个文件中, 这时这个文件中存储的订单顺序就是从小到大有序的了.




计数排序

计数排序其实是桶排序的一种特殊情况. 当数据最大值是 k 时, 直接划分 k 个桶, 这样省掉了桶内排序的时间.

时间复杂度 O(n + k), 需要存储空间 O(n + k)

**应用场景: **

计数排序只能用在数据范围不大的场景中, 如果数据范围 k 比要排序的数据 n 大很多 (非常分散), 就不适合计数排序了.

而且, 计数排序只能给非负整数排序. 其他类型的数据要在不改变相对大小的情况下映射到非负整数.

不稳定的

public static void bucketSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    int max = Integer.MIN_VALUE;
    //找出数组中最大值
    for (int i = 0; i < arr.length; i++) {
        max = Math.max(max, arr[i]);
    }
    //创建 max + 1 个桶, 并添加
    int[] bucket = new int[max + 1];
    for (int i = 0; i < arr.length; i++) {
        bucket[arr[i]]++;
    }
    int i = 0;
    //不稳定的取数方法
    for (int j = 0; j < bucket.length; j++) {
        while (bucket[j] > 0) {
            arr[i++] = j;
            bucket[j]--;
        }
    }
}

稳定的

public static void bucketSort(int[] arr) {
    //因为计数排序是桶排的特殊形式, 这里数组名定义为bucket
    if (arr == null || arr.length < 2) {
        return;
    }
    int max = Integer.MIN_VALUE;
    //找出数组中最大值
    for (int i = 0; i < arr.length; i++) {
        max = Math.max(max, arr[i]);
    }
    //创建 max + 1 个桶, 并添加
    int[] bucket = new int[max + 1];
    for (int i = 0; i < arr.length; i++) {
        bucket[arr[i]]++;
    }
    //依次累加
    for (int i = 1; i < bucket.length; i++) {
        bucket[i] = bucket[i - 1] + bucket[i];
        //此时bucket存储 <= 分数i的考生个数
    }

    //临时数组 help, 存储排序之后的结果
    int[] help = new int[arr.length];

    for (int i = arr.length - 1; i >= 0; i--) {
        int index = bucket[arr[i]] - 1;
        help[index] = arr[i];
        bucket[arr[i]]--;
    }

    //将结果复制回arr
    for (int i = 0; i < arr.length; i++) {
        arr[i] = help[i];
    }
}



基数排序

怎么将10w个手机号码从小到大排序?

先按照最后一位排序, 再按照倒数第二位重新排序…最后按照第一位重新排序.

根据每一位来排序, 可以用计数(桶)排序的方法, O(n), 有 k 位, 时间复杂度是 O(k*n), 稳定

(如果位数不等长可以补0)

**应用场景: **

基数排序对要排序的数据是有要求的, 需要可以分割出独立的比较单元, 位. 而且位之间有递进的关系.

每一位的数据范围不能太大, 要可以用线性排序算法来排序, 否则, 基数排序的时间复杂度就无法做到 O(n) 了

对扑克牌排序

设置4个桶, 分别存红桃,黑桃, 梅花, 方片, 桶内顺序排序.

取出时, 依次取出4个桶中最大的数.




总结

桶排序应用场景:

  • 要排序的数据需要很容易就能划分成 m 个桶
  • 桶与桶之间有着天然的大小排序
  • 数据在各个桶之间要求分布均匀

计数排序应用场景

  • 数据范围不大, 不过于分散
  • 只能给非负整数排序

基数排序应用场景

  • 数据可以分割出独立的比较单元, 位
  • 位之间有递进的关系
  • 数据范围不能太大 (用基数排序来按位排序)

猜你喜欢

转载自blog.csdn.net/weixin_41889284/article/details/88321415