前面介绍的算法都有一个共同的性质:排序结果中,各元素的次序基于输入时间的比较,我们把这类排序算法称为比较排序。
8.1比较排序算法的时间下界
决策树模型
比较排序的过程可以被抽象地视为决策树。一棵决策树是一棵满二叉树,表示某排序算法作用于给定输入所做的所有比较。排序算法的执行对应于遍历一条从树的根到叶节点的路径。每个内结点对应一个比较ai&aj,左子树决定着ai<=aj以后的比较,右子树决定着ai>aj以后的比较。当到达一个叶节点时,排序算法就已确定。排序算法能够正确工作的的必要条件是,n个元素的n!种排列都要作为决策树的一个叶节点出现。设决策树的高度为h,叶子数目为l,那么有 2h>=l>=n!, 于是有 h>lgn! = Ω(nlgn)。 这说明比较排序的最坏时间复杂度为Ω(nlgn)。这也说明合并排序和堆排序的复杂度已经渐进最优了。
练习:
8.1-1 在比较排序的决策树中,一个叶节点最小可能的深度是多少?
分析: n-1。因为至少要比较n-1次。不知道有没有更加理论化的证明?
8.1-3 证明:对于长度为n的n!种输入中的至少一半而言,不存在具有线性时间的比较排序算法。对n!的1/n部分而言又怎样?1/2n部分呢?
分析:假设在决策树种,m个叶节点的深度为 h =O(n);那么有2h > m,于是可知 h为Ω(lgm)。将m=n!/2代入,可知这与h=O(n)相矛盾。同样,对于1/n*n!和1/2n*n!也一样。
8.1-4 现有n个元素要排序,输入序列为n/k个子序列,每个包含k个元素,每个子序列中的元素都小于后继子序列中的元素,大于前驱子序列中的元素。这样只要对个子序列中的k各元素进行排序就可以得到对整个输入序列的排序结果,证明:这个排序问题中所需的比较次数有一个下界Ω(nlgk)。
分析: 每个k元素子序列的排列数目为k!,那么整个序列在上述条件下的排列数目为(k!)n/k。按决策树的分析方法,决策树的深度为h>lg((k!)n/k) = n/k lg (k!)>n/k lg (k/2)k/2= n/2lgk/2。因此 h=Ω(nlgk)。
8.2计数排序
计数排序假设n个输入元素的每一个都是介于0到k之间的整数,此处k为某个整数。当k=O(n)时,计数排序的运行时间为Θ(n)。
计数排序的思想就是对每一个输入元素x,确定出小于x的元素个数。有了这一信息,就可以把x直接放到最终输出数组的为位置上了。
下面是计数排序的伪码,假定输入是数组A[1...n], 存放排序结果的B[1...n],以及提供计数临时存储的C[0...k]。
COUNTING-SORT(A,B,k)
1 for i <-- 0 to k
2 do C[i] <-- 0
3 for j <-- 1 to length[A]
4 do C[A[j]] <-- C[A[j]]+1
5 for i <-- 1 to k
6 do C[i] = C[i] + C[i-1]
7 for i <-- length[A] downto 1
8 do B[C[A[i]] = A[i]
9 C[A[i]] = C[A[i]] -1
计数排序的运行时间为Θ(n+k), 且计数排序是稳定的。计数排序之所以能够突破前面所述的Ω(nlgn)极限,是因为它不是基于元素比较的,它是基于元素值的先验知识的。计数排序虽然渐进复杂度上要优于比较排序,但它的常数因此明显较大,而且不是就地排序。所以在实际的选择上,还要考虑到输入的特点,输入的规模,内存限制等因素。
练习:
8.2-4请给出一个算法,使之对给定的介于0到k之间的n个整数进行预处理,并能在O(1)时间内回答出输入的整数中有多少个落在[a...b]区间内。你给出的算法的预处理时间为Θ(n+k)。
分析:就是用计数排序中的预处理方法,获得数组C[0...k],使得C[i]为不大于i的元素的个数。这样落入[a...b]区间内的元素个数有C[b]-C[a-1]。
8.3基数排序
假设长度为n的数组A中,每个元素都有d位数字,其中第一位是最低位,第d位是最高位,基数排序的算法如下:
RADIX-SORT(A,d)
1 for i <-- 1 to d
2 do use a stable sort to sort array A on digit i
引理8.3: 给定n个d位数,每一个数位可以取k种可能的值,如果所用稳定排序需要Θ(n+k)时间,基数排序能以Θ(d(n+k))的时间完成。
证明:如果采用计数排序这种稳定排序,那么每一遍处理需要时间Θ(n+k),一共需处理d编,于是基数排序的运行时间为Θ(d(n+k))。
在将关键字分成若干位方面,我们又一定的灵活性。
引理8.4:给定n个b位数和任何正整数r<=b,RADIX-SORT能在Θ((b/r)(n+2r))时间内正确地对这些数进行排序。
证明:对于一个r<=b,将每个关键字看成由d=b/r位组成的数,每一个数字都是(0~2r-1)之间的一个整数,这样就可以采取计数排序了。总的运行时间为Θ((b/r)(n+2r))。
对于给定的n值和b值,如何选择r值使得最小化表达式(b/r)(n+2r)。如果b< lgn,对于任何r<=b的值,都有(n+2r)=Θ(n),于是选择r=b,使计数排序的时间为Θ((b/b)(n+2b)) = Θ(n)。 如果b>lgn,则选择r=lgn,可以给出在某一常数因子内的最佳时间:当r=lgn使,算法复杂度为Θ(bn/lgn),当r增大到lg以上时,分子2r增大比分母r快,于是运行时间复杂度为Ω(bn/lgn);反之当r减小到lgn以下的时候,b/r增大,而n+2r仍然是Θ(n)。
练习:
8.3-4 说明如何在O(n)时间内,对0~n2-1之间的n个数进行排序。
分析:依据引理8.4,取b=lg(n2) = 2lgn, r = lgn。基数排序的时间复杂度为Θ(2(n+n)) = Θ(n)。
8.4桶排序
计数排序假设输入是由一个范围内的整数构成,而桶排序假设输入是由一个随机过程产生,该过程均匀而独立地分布在区间[0,1)上。
桶排序的思想就是把区间[0,1)划分成n个相同大小的子区间,或称桶。然后将n个数分布到各个桶中去。由于分布是均匀的,所以不会有很多数落在一个桶中的情况,为了得到结果,先对各桶中的元素进行排序,然后按次序把各桶中的元素列出来即可。
以下是桶排序的代码,假设输入的是一个包含n个元素的数组A,每个元素满足A[i]<1。另外B[0...n-1]是一个存放桶的数组,并假设可以采用某种机制来维护这些表。
BUCKET-SORT(A)
1 n <-- length[A]
2 for i <-- 1 to n
3 do insert A[i] into list B[nA[i]]
4 for i <-- 0 to n-1
5 do sort list B[i] with insertion sort
6 concatenate the lists B[0],B[1],...,B[n-1] together in order
桶排序的期望时间分析
设ni为第3行后,各桶中的元素数量,于是算法的运行时间可表示为 T(n) = Θ(n) + ∑i=0~n-1O(ni2)。
现分析某个桶元素的数量期望值。假设指示器随机变量 Xij = I{A[j]落在桶i中}。 P{Xij=1} = 1/n。
ni = ∑j=1~nXij
E[ni2] = E[∑j=1~nXij * ∑j=1~nXij] = E[∑j=1~nXij2+∑j=1~n∑k=1~n,k!=jXij Xik] = ∑j=1~nE[Xij2] + +∑j=1~n∑k=1~n,k!=jE[Xij Xik]
E[Xij2] = 1/n
E[XijXik] = 1/n*1/n = 1/n2
于是E[ni2] = 2-1/n。
可知E[T(n)] = Θ(n)。
即使输入不满足均匀分布,桶排序可以以线性时间运行,只要满足各个桶尺寸的平方和与总的元素数量呈线性关系。
练习:
8.4-4. 在单位圆中有n个点,pi = (xi,yi),使得0<xi2+yi2<=1, i=1,2,...,n。假设所有的点式均匀分布的,亦即点落在落在圆的任意区域的概率与该区域的面积成正比。请设计一个Θ(n)期望时间的算法,来依据点到原点之间的距离对n个点排序。
分析:假设圆的面积为S,那么如果可以将分成每个面积S/n的n个区域,又由于我们的目标是按点掉圆心的距离排序,所以划分区域的方式是围绕圆心,一圈一圈地划分,划分的方法如下:选定一些列的距离d0,d1,...,dn,满足d0=0, πd12=S/n, πd22=2*S/n,...,πdn2=S。 那么离圆心距离为di-1到di之间的区域构成了桶i。
思考题: