-
前言
快速排序(Quick Sort)是目前应用最广泛的排序算法,因其时间复杂度低和内循环较小,而且不需要太多的额外空间,广泛应用于工业界。比如JDK源码中的排序算法就是使用的快速排序。
虽然快速排序应用广泛,但其最优时间复杂度仍为O(NlogN),不是O(n)。那么今天,我就给大家介绍几种时间复杂度为O(n)的排序算法。
-
总论
我们把时间复杂度为O(n)的排序算法叫做线性排序,常见的线性排序算法有:桶排序、计数排序、基数排序。这几种算法之所以能做到线性时间内完成排序,是因为其不是基于比较的排序算法。下面我详细介绍下这几种排序算法。
-
排序算法介绍
桶排序
桶排序顾名思义就是将数据分几个有序的桶里,然后每个桶内单独进行排序。桶内排完序之后,再按顺序把每个桶内的数据依次取出,组成的序列就是有序的了。
图示就是桶排序的例子,我们根据这个例子分析下桶排序的时间复杂度。
如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶,每个桶都有k=n/m个元素。如果桶内部使用快速排序的话,那每个桶内排序的时间复杂度为O(k*logk),m个桶进行排序的时间复杂度为O(m*k*logk)。因为k=n/m,所以整个桶排序的时间复杂度为O(n*logn/m)。当桶的个数m接近数据个数n时,logn/m就是个非常小的常数,这时候桶排序的时间复杂度就是O(n)。
桶排序是否替代广泛使用的快速排序
当然不能,虽然桶排序时间复杂度能达到O(n),但是它对要排序的数据要求非常严格。1,要排序的数据需要很容易划分到m个桶中,且桶与桶之间有着天然的大小顺序。数据只需要在桶内进行排序后,数据总体就已经有序了。2,数据在各个桶中的分布是比较均匀的。如果数据划分到各个桶中后,有些桶内数据非常多,有些非常少,很不平均,那各个桶内数据排序的时间复杂度就不是一个数量级了。在极端情况下,所有的数据都划分到同一个桶内,那就退化为O(NlogN)时间复杂度的排序算法了。
计数排序
我觉得,计数排序是桶排序的一种特殊情况。当要排序的n个数据,所处的数据范围并不大,且最大值与最小值的差值为k,那么我们就可以将数据划分为k个桶。每个桶内的数据数值是相同的,这样就省掉了桶内排序的时间。
下面根据一个示例来讲解,比如现在有个待排序的整数序列A={-1, 2, 0, 4, 3, 6, 5, 8, -2, 1, 3, 0, 3,6, 5, 2}。首先我们花O(n)的时间扫描一下整个序列,可以得到max=8,min=-2。然后我们建立一个新的数组C,长度为(max-min+1)=11。数组C如下所示:
数组index为0的元素记录的值是-2出现的次数,依次index为1的元素记录的是-1出现的次数。
此时我们再扫描一下数组A,比如对于-1,我们的操作是:-1-min=-1-(-2)=1;C[1]++。对于2,我们的操作是:2-(-2)=4;C[4]++。这样我们又花了O(n)的时间。操作结果是:
最后我们便可以输出目标整数序列了,具体的逻辑是遍历数组C,比如C[0]=1,则输出一次min+index即(-2+0)=-2;C[1]=1,则输出一次-1;C[2]=2,则输出两次0,以此类推。目标序列为:
-2, -1, 0, 0, 1, 2, 2, 3, 3, 3, 4, 5, 5, 6, 6, 8
分析可得,我们扫描了两次数组A,一次数组C,所以计数排序的时间复杂度为2(n)+n+k(扫描数组C的时间复杂度是n+k)。空间复杂度是:n+k(n是数组A的空间,最后输出目标整数序列时可以直接存储在A中;k是数组C的空间)。故其空间复杂度和时间复杂度均为O(n+k)。
计数排序只能适用于数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型,要将其在不改变相对大小的情况下,转化为非负整数(如上例)。
基数排序
基数排序的主要思路是,将所有待比较数值(必须是正整数)统一为同样的数位长度,数位较短的数前面补零(补零一般不影响排序结果,如果出现影响排序结果的话,就要考虑如何处理)。然后, 从最低位开始, 依次进行一次稳定排序(因为每个位可能的取值范围是固定的从0到9).这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
比如这样一个数列排序: 342 58 576 356, 以下描述演示了具体的排序过程(加黑字体表示正在排序的数位)
第一次排序(个位):
3 4 2
5 7 6
3 5 6
0 5 8
第二次排序(十位):
3 4 2
3 5 6
0 5 8
5 7 6
第三次排序(百位):
0 5 8
3 4 2
3 5 6
5 7 6
结果: 58 342 356 576
每一位排序,我们可以用刚讲过的桶排序或者计数排序,它们的时间复杂度为O(n)。我们要排序的数据有k位,那么算法总的时间复杂度就为O(k*n),当k不大时,那么基数排序的算法时间复杂度就近似于O(n)。
-
总结
为什么存在这么多线性时间就能完成的排序方式鲜有人知,反而时间复杂度更高的快速排序却广泛应用呢
1.之前提到了这些线性排序对于要排序的数据要求严格,而快速排序对数据要求不高且优化后出现时间复杂度O(n2)的概率极低;
2.线性排序的时间复杂度虽然是O(n),但是其做了很多假设,真实情况下复杂度n前面的常量因子很大,但相同规模中,快速排序复杂度前的常量因子较小。
3.所有的线性排序都需要一定的额外空间复杂度,而快速排序需要的额外空间复杂度是O(1).
虽然从时间复杂度上说这些线性排序都比快速排序快,但从以上分析可知实际应用中可能并不如快速排序。这也教给我们,判断排序算法好坏的标准,不关只有时间复杂度,还有空间复杂度、问题的规模、应用的场景、数据的特点等多方面的因素。所以,算法无所谓好坏,就看使用的方式是不是正确。
大家如果想深入了解这些,可以报名下图这个课程,一起学习算法,打好基础,坚实地向前迈出每一步。