困难的不是算法的思想,而是证明时间复杂度是线性。首先可以修改快排的Partition步骤,获得一个期望时间为O(n)的算法,这里姑且称之为算法1。用一些特别的办法改进Partition时主元的选择,可以获得一个最坏情况下时间复杂度为O(n)的算法,这里姑且称之为算法1a
算法1
步骤:
- 任选一个元素x作为主元,应用快排Partition的对数组进行分块(小的在前),记分块后x的下标为k
- 若k<⌊n/2⌋,那么说明中位数大于x,对右侧执行第一步
- 若k>⌊n/2⌋,那么说明中位数小于x,对左侧执行第一步
- 若k=⌊n/2⌋,那么x就是中位数
Partition伪代码如下
partition(A[], l, r) {
i = l-1, j = l
p_i = random(l, r) // 等概率在[l, r)中选取一个整数
swap(A[r-1], A[p_i])
pivot = A[r-1]
while (j < r) {
if (A[j] <= pivot) {
i = i + 1
swap(A[j], A[i])
}
++j
}
return i // 返回分块后主元的位置
}
下面分析时间复杂度,可以得到递归式
T(n)=T(max{n−k,k})+O(n)
我们假设选取主元是随机的,并且每个元素有相等的概率(即P=1/n)被选为主元,那么对上式取期望
E[T(n)]=E[T(max{n−k,k})+O(n)]=E[T(max{n−k,k})]+O(n)=n1i=0∑n−1E[T(max{n−k,k})]+O(n)
注意到,当k<⌊n/2⌋时,n−k≥k,否则k≥n−k,那么在上面的那个合式中,T(⌊n/2⌋+1),T(⌊n/2⌋+2),…,T(n−1)出现了两次,所以得到
E[T(n)]=n1i=0∑n−1E[T(max{n−k,k})]+O(n)≤n2i=⌊n/2⌋∑nE[T(i)]+O(n)
我们假设存在n0,使n<n0时有E[T(n)]=O(1)
那么n=n0时,有
E[T(n)]=O(n)
n0的存在是合理的,规模很小的时候,可以视为O(1)。因为递归到最后,肯定有个终止条件。比如说在这个算法中,分块的区间等于1的时候,我们可以立即得到结果。
因此我们有理由假设一直到n1,都存在c使得E[T(n)]≤cn,当n>n1时,把上式带入递归式
E[T(n)]≤n2c2(⌊n/2⌋+n)⌊n/2⌋+O(n)=O(n)
由此证明了E[T(n)]=O(n)
不过这个算法最坏情况是O(n2)的。
考虑一直选取最小或者最大的元素作为主元的情况。
算法1a
虽然说算法1实现起来并不困难,但是最坏时间有时候会退化到O(n2),其实我们完全有办法改进选取主元的方式,使之最坏情况下时间复杂度是O(n)。
改进后的选取主元的步骤:
- 将数组切分成⌊n/5⌋个长度为5的数组(如果最后一组不足5个元素,就丢弃掉这组,所以是向下取整)
- 找出每个切分后的组的中位数,逻辑上构成一个新的数组B
- 调用本算法,找到中位数数组的中位数,将其作为算法1第一步的主元
在数组B的中位数大于至多⌊7n/10⌋个元素,小于至多⌊7n/10⌋个元素。那么算法1的递归式可以改成。
T(n)≤T(107n)+O(n)
同样假设有T(n)≤cn,代回即可验证这一假设是正确的
T(n)≤107cn+O(n)=O(n)