线性时间排序

证明:所有基于比较模型(comparison model 通过比较两个元素的大小以确定元素的位置)的排序算法的运算速度不会快于O(nlogn)

比如快速排序(nlogn),堆排序(nlogn),归并排序(nlogn),插入排序(n2)……等等,都是基于比较模型

为了证明该结论,引入决策树这个工具

例如,假设要排序三个元素,a,b,c , 构造的决策树如下所示

        a > b?

          /      \

       b>c?       a>c?

         /    \           /    \

          a,b,c    a>c?    b,a,c   b>c?

        /    \               /   \

                 a,c,b c,a,b     b,c,a  c,b,a

 

其中的每个节点表示一次比较,最后的叶子节点表示所有可能的比较结果,因此,决策树的定义如下:

假设待排序<a1,a2,a3…………an>长度为n

1、树种的每个非叶节点都会有两个下标 i , j (  1<= i,j <=n ) ,表示要比较ai和aj的大小

2、每个非叶节点会分裂成两个子树,其中左边的子树说明ai<=aj的情况下的后续情况,右侧子树说明ai>aj情况下的后续情况

3、每个叶子节点表示一种排序结果情况

这样的决策树就是一个二叉树,所有基于比较模型的排序算法都可以转换成决策树模型的方式,现在用决策树来处理比较排序

1、为待排序列n构造一个决策树

2、在进行比较时,将树分成两个分支左子树和右子树。决策树就是把算法中这些比较的所有可能的结果分别列出来,指出了所有可能的路线

这样,对于长度为n的待排序列,根据排列组合原理,总的可能的排序结果有n!个,由此可见决策树增长很快(指数级),这也是为什么一般不用决策树来描述算法,不够简洁。不过用决策树来进行算法分析的话就很有用了,决策树代表了算法执行的所有可能,如果找出一条特定的算法执行路径,即一条从跟节点到叶子节点的路径

路径的深度=算法运行时间

树的高度=算法的最坏运行时间

现在,就需要证明树的高度至少是nlogn,即如果决策树对n个元素排序,则该决策树的高度至少是nlogn

已知:决策树的叶子节点数为n!个,根据二叉树的性质可知,对于一个高度为h的二叉树,其叶子节点数最多为2h个,即可得到一个不等式

    n!≤2

两边取对数,得到

    h≥logn!

斯特林公式可知

    h≥logn!

    ≥log(n/e)n

    =nlogn-nloge=O(nlogn)  证毕

同样可以证明,在加入随机化过程的比较模型中,也要至少经过nlogn次比较。

因此,想要得到线性速度的排序算法(不能比线性速度更快了,因为无论如何都要遍历一遍排序序列),就必须舍弃基于比较的模型,而是对元素做一些其他的操作。

计数排序(counting sort)

计数排序是一个线性时间的排序算法,算法如下:

1、待排序列A <a1,a2,a3……an>长度为n

2、序列中的每个元素ai的数值范围都在[1,k]之间,实际的运行时间也会依赖于k

3、排好序后的序列存放到B <b1,b2,b3……bn>中,同时需要一个长度为k的辅助存储序列C <c1,c2,c3……ck>

void CountingSort(int A[],int B[],int n) {
    int C[k]; int i; 
    for(i=0;i<k-1;i++)
        C[i]=0;
    for(i=0;i<n-1;i++)
        C[A[i]] += 1; 
    for(i=1;i<k-1;i++)
        C[i] += C[i-1]; 
    for(i=n-1;i>=0;i--){ 
        B[C[A[i]]] = A[i];
        C[A[i]] -=1; 
    } 
} 

唔,很简洁,也很令人迷惑,举例如下:

假设A=[4,1,3,4,3] 可见数组中的元素最大不超过4,于是k=5(从0到4),因此初始化C=[0,0,0,0,0]

然后循环序列A,并对C中的元素赋值,整个过程如下

C=[0,0,0,0,1]

C=[0,1,0,0,1]

C=[0,1,0,1,1]

C=[0,1,0,1,2]

C=[0,1,0,2,2]

然后循环序列C,求和

C=[0,1,0,2,2]

C=[0,1,1,2,2]

C=[0,1,1,3,2]

C=[0,1,1,3,5]

最后得到的序列C表示,总共有5个元素,其中小于等于3的有3个,小于等于2的有1个,小于等于1的有1个,小于等于0的没有。

最后一个循环得到结果序列B

 A=[4,1,3,4,3] , B=[  ,  ,  , ,  ] , C=[0,1,1,3,5]

i=4 , A[i]=3 , C[A[i]]-1=C[3]-1=2 , B[C[A[i]]-1]=B[2]=3 , B=[ , ,3, , ] , C=[0,1,1,2,5]

i=3 , A[i]=4 , C[A[i]]-1=C[4]-1=4 , B[C[A[i]]-1]=B[3]=4 , B=[ , ,3, ,4] , C=[0,1,1,2,4]

i=2 , A[i]=3 , C[A[i]]-1=C[3]-1=1 , B[C[A[i]]-1]=B[1]=3 , B=[ ,3,3, ,4] , C=[0,1,1,1,4]

i=1 , A[i]=1 , C[A[i]]-1=C[1]-1=0 , B[C[A[i]]-1]=B[0]=1 , B=[1,3,3, ,4] , C=[0,0,1,1,4]

i=0 , A[i]=4 , C[A[i]]-1=C[4]-1=3 , B[C[A[i]]-1]=B[3]=4 , B=[1,3,3,4,4] , C=[0,0,1,1,3]

运行时间T(n)=O(k)+O(n)+O(k)+O(n)=O(k+n)

因此,当k比较小的话(K<n),则T(n)=O(n)。

在实际场景中,如果要处理的数据大小很小,比如只有一个字节的话,那么k=256,那么这个算法就很快了O(256+n)

而如果要处理的数据是一些32位的整形数据的话,那么k=232=4G,那么辅助数组C就需要4G*4B=16GB的内存空间,光初始化数组C就得费老大劲了,况且谁有16G内存的电脑可供挥霍呢……

这儿计数排序有一个很重要的特点:排序的稳定性

即待排序列中相等元素在排序过后的相对位置保持不变。计数排序就是一个稳定的排序,可以看到原序列中有两个3和两个4,排序前位置靠前的3和4在排序后位置依然靠前。基数排序就是基于稳定的计数排序,也是一个线性时间的排序算法。

基数排序

基数排序是最古老的排序方法之一,大约出现于1890年由Herman Hollerith发明。Herman发明了最早版本的打孔机和卡片制表系统,在1890年的时候,美国的人口普查是一件很艰难的工作,Herman利用他的发明的制表系统帮助美国政府统计人口,赚了很大一笔钱,他的公司后来发展成现在的IBM。由此可见,科技真是第一生产力。

Herman在他的制表系统中使用的排序方法是:使用一种稳定的排序算法先对n个数的最低位进行排序,然后对n个数的次低位进行排序,按照这样的顺序排序下去,最后给n个数的最高位进行排序,这样这n个数就整体有序了。(低位优先排序法)

即整个算法的排序思想就是按位排序,从最低位排到最高位,想出这个算法的人真是个天才。例如对下面7个数排序:

329  720  720  329

457  355  329  355

657  436  436  436

839 -->457-->839-->457

436  657  355  657

720  329  457  720

355  839  657  839

最后结果就是有序的了,现在证明其正确性:

1、对已排序的部分按照数字位进行归纳,记为t,即通过归纳假设在t之前的t-1位已经排序

2、然后对t位排序。

3、排序过程中,如果有两个元素是相同的,即有相同的第t位数字,基于稳定性原则,那么就不能交换其顺序。根据归纳假设可知这t位仍然保持有序

4、如果有不同的第t位数字,那么排序过程就将他们调整为正确的顺序,由此得到有序的序列,那么这t位依然保持有序(高位大,无论低位如何,数就是大的)。证毕

使用计数排序作为辅助算法来实现基数排序,这样便克服了计数排序无法排序数据范围比较大的数的缺点,同时保证了线性的时间复杂度。

比如,对于n个32位的整数,可以将这32位的数分为4个部分,每个部分8位,然后使用计数排序分别对这4个部分进行排序即可。

那么,对于n个b位的整数,如何进行划分才能获得最大效率呢?

假设将b位的整数划分为b/r个部分,每个部分有r位,用计数排序对r位数据进行排序,可知k=2r,计数排序花费时间为O(2r+n),总共需要进行b/r轮的计数排序,则总用时为:

  T(n)=O[b/r(2r+n)]

其中的r是为我们所掌控的,通过选择适当的r来减少运行时间。想要得到线性的运行时间,只需要

  (b/r)*2r  ≤ (b/r)n

就可以了,这样对于O符号就可以忽略低阶项,两边取对数,得

  r ≤ logn

取r=logn带入T(n),同时忽略低阶项得

  T(n)=O(bn/logn)

其中b表示整数的位数,整数的表示范围为2b-1,同时也可以表示为nd-1,两边取对数得到

  b=dlogn

带入T(n)得到

  T(n)=O(d*n) 

其中d是一个常数,只要d<logn,那么整个的排序速度就比nlogn的比较模型的排序算法速度快了

posted on 2012-07-08 14:34  len_sround  阅读(321)  评论(0编辑  收藏  举报

导航