之前提到的排序算法有归并排序、堆排序、快速排序、插入排序,这些排序都是基于比较的,因此称这类排序为比较排序。比较排序的下界为 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn),其中快速排序、堆排序、归并排序都可以达到这个时间性能。在本篇博客,将介绍几个线性时间排序算法,线性时间排序算法指的是排序 n n n个元素使用的时间为 Θ ( n ) \Theta(n) Θ(n)。
计数排序的伪代码如下:
COUNTING-SORT(A,B,k)
let C[0..k] be a new array
for i=0 to k
C[i]=0
for j=1 to A.length
C[A[j]]=C[A[j]]+1
// 此时C[i]中为待排序列A中数字i的数量
for i=1 to k
C[i]=C[i]+C[i-1]
//此时C[i]中为A中小于i或等于数字i的数量
for j=A.length downto 1
B[C[A[j]]]=A[j]
C[A[j]]=C[A[j]]-1
上述方法实现的功能是排序A,并将排序好的序列输出到B,其中传入参数k为A中的数字范围,以序列 A = [ 2 , 5 , 3 , 0 , 2 , 3 , 0 , 3 ] A=[2,5,3,0,2,3,0,3] A=[2,5,3,0,2,3,0,3]为例, A A A中的数值范围为0~5,因此k为5,具体排序步骤如下:
图(a)中是执行完第二个for循环之后的C中的值,其中C[0]=2代表A中为0的数字有两个,其余的依次类推;图(b)中是执行完第三个for循环之后C中的值,其中C[0]=2代表A中小于0或等于0的数字有2个,C[2]=4代表A中小于2或等于2的数字有4个,其余的依次类推。
图(c)是第四个for循环执行完一轮之后B跟C的状态,这个for循环的作用是将原序列 A = [ 2 , 5 , 3 , 0 , 2 , 3 , 0 , 3 ] A=[2,5,3,0,2,3,0,3] A=[2,5,3,0,2,3,0,3]从最后一个元素有序的放到B中。这里仅仅说明了代码的作用,并没有说明具体怎么实现的,伪代码的实现认真看伪代码和例子与配图就很容易理解了。
分析伪代码可知,第一个跟第三个for循环花费的时间为 O ( k ) O(k) O(k),第二个跟第四个for循环花费的时间为 O ( n ) O(n) O(n);因此计数排序花费的时间为 O ( n + k ) O(n+k) O(n+k),其中k为排序数列中数字的范围,n为排序数列中数字的个数。
当 k < n k
基数排序看起来比较简单,比如有n个b位数,从小到大分别排序每一位,就完成排序了如下所示,先排序个位数,再排序十位数,最后排序百位数。
若排序每一位数采用的是计数排序的话,每一轮计数排序花费时间为 O ( n + k ) O(n+k) O(n+k),总共b轮,因此总共花费时间为 O ( b ( n + k ) ) O(b(n+k)) O(b(n+k));
因此在每一轮采用技术排序,且 n > k n>k n>k,b为常数的情况下,基数排序的时间为 O ( n ) O(n) O(n);
桶排序是计数排序的直接升级版,只不过将计数排序中的C的每一位数变成一个“桶“,桶里放着一个范围的数,先排序每个桶里的数然后在将桶串起来,桶排序的期望运行时间为 O ( n ) O(n) O(n)。
本篇博客介绍了几种线性时间排序,其中基数排序跟桶排序都是基于计数排序的,桶排序花费的时间的具体证明比较麻烦,这里未进行详细描述。