算法(线性时间复杂度的排序)

线性时间复杂度的排序算法
 有桶排序、计数排序、基数排序


桶排序(Bucket sort)    
核心思路就是将要排序的数据分到几个有序的桶里,每个桶单独排序,依次从桶里把数据取出,这时候数据就有序了


桶排序的时间复杂度为什么是 O(n) 呢?我们一块儿来分析一下。
如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快
速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排
序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个
时候桶排序的时间复杂度接近 O(n)。
桶排序看起来很优秀,那它是不是可以替代我们之前讲的排序算法呢?
答案当然是否定的。为了让你轻松理解桶排序的核心思想,我刚才做了很多假设。实际上,桶排序对要排序数据的要
求是非常苛刻的。
首先,要排序的数据需要很容易就能划分成 m 个桶,并且,桶与桶之间有着天然的大小顺序。这样每个桶内的数据
都排序完之后,桶与桶之间的数据不需要再进行排序。
其次,数据在各个桶之间的分布是比较均匀的。如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,
很不平均,那桶内数据排序的时间复杂度就不是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化
为 O(nlogn) 的排序算法了。
桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数
据全部加载到内存中。
比如说我们有 10GB 的订单数据,我们希望按订单金额(假设金额都是正整数)进行排序,但是我们的内存有限,只
有几百 MB,没办法一次性把 10GB 的数据都加载到内存中。这个时候该怎么办呢?
现在我来讲一下,如何借助桶排序的处理思想来解决这个问题。
我们可以先扫描一遍文件,看订单金额所处的数据范围。假设经过扫描之后我们得到,订单金额最小是 1 元,最大
是 10 万元。我们将所有订单根据金额划分到 100 个桶里,第一个桶我们存储金额在 1 元到 1000 元之内的订单,
第二桶存储金额在 1001 元到 2000 元之内的订单,以此类推。每一个桶对应一个文件,并且按照金额范围的大小顺
序编号命名(00,01,02…99)。
理想的情况下,如果订单金额在 1 到 10 万之间均匀分布,那订单会被均匀划分到 100 个文件中,每个小文件中存
储大约 100MB 的订单数据,我们就可以将这 100 个小文件依次放到内存中,用快排来排序。等所有文件都排好序之
后,我们只需要按照文件编号,从小到大依次读取每个小文件中的订单数据,并将其写入到一个文件中,那这个文件
中存储的就是按照金额从小到大排序的订单数据了。
不过,你可能也发现了,订单按照金额在 1 元到 10 万元之间并不一定是均匀分布的 ,所以 10GB 订单数据是无法
均匀地被划分到 100 个文件中的。有可能某个金额区间的数据特别多,划分之后对应的文件就会很大,没法一次性
读入内存。这又该怎么办呢?
针对这些划分之后还是比较大的文件,我们可以继续划分,比如,订单金额在 1 元到 1000 元之间的比较多,我们
就将这个区间继续划分为 10 个小区间,1 元到 100 元,101 元到 200 元,201 元到 300 元…901 元到 1000 元
。如果划分之后,101 元到 200 元之间的订单还是太多,无法一次性读入内存,那就继续再划分,直到所有的文件
都能读入内存为止。

计数排序(Counting sort)

 当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内
的数据值都是相同的,省掉了桶内排序的时间。


    public static void countingSort(int[]a,int n){// 计数排序(相当于特殊的桶排序)

        int max = a[0];// 获取数组最大值
        for (int i = 1; i < n; i++) {
            if (max < a[i]){
                max = a[i];
            }
        }

        // 创建max+1个桶,主要是数组下标为0开始,所以需要加1。a的最大值在在桶的下标中必须要有,不然就
数据溢出了。
        int [] c = new int[max+1];
        for (int i = 0; i <max+1; i++) {
            // 给桶附默认值
            c[i]=0;
        }

        // 当 数组a的数据=桶的下标 时,桶的值加1,表示桶中存在多少个和桶下标一样的元素。
        for (int i = 0; i < n; i++) {
            c[a[i]]++;
        }

        // 桶中的值累加,加完之后,桶中存放的值为数组a排好序的下标。(如果有重复,则存放的为最后一个下
标,下一个相同的值则依次往前一个下标存放)
        for (int i = 1; i < max+1; i++) {
            c[i] = c[i] + c[i-1];
        }

        // 创建临时数组,用来存放排序结果
        int [] ret = new int[n];

        // 排序操作:
        for (int i = n-1; i >= 0; i--) {
            int idx = c[a[i]];// 数组a的值在排序数组ret的下标。
            ret[idx-1] = a[i];// 给排序结果数组ret赋值
            c[a[i]]--;// 赋完值之后再出现相同的元素则放到当前下标的前面一位。
        }

        // 将排好序的数组ret赋值给数组a
        for (int i = 0; i < n; i++) {
            a[i] = ret[i];
        }
    }


基数排序(Radix sort)

基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数
据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算
法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。

总结:

这几种线性时间复杂度排序算法在特定情况下都比较高效,但是一般很少使用

桶排序和计数排序的排序思想是非常相似的,都是针对范围不大的数据,将数据划分成不同的桶来实现排序。
基数排序要求数据可以划分成高低位,位之间有递进关系。比较两个数,我们只需要比较高位,高位相同的再比较低位。而且每一位的数据范围不能太大,因为基数排序算法需要借助桶排序或者计数排序来完成每一个位的排序工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值