困难的不是算法的思想,而是证明时间复杂度是线性。首先可以修改快排的Partition步骤,获得一个期望时间为O(n)O(n)的算法,这里姑且称之为算法1。用一些特别的办法改进Partition时主元的选择,可以获得一个最坏情况下时间复杂度为O(n)O(n)的算法,这里姑且称之为算法1a

算法1

步骤:

  1. 任选一个元素xx作为主元,应用快排Partition\text{Partition}的对数组进行分块(小的在前),记分块后xx的下标为kk
  2. k<n/2k\lt \lfloor n/2\rfloor,那么说明中位数大于xx,对右侧执行第一步
  3. k>n/2k\gt \lfloor n/2\rfloor,那么说明中位数小于xx,对左侧执行第一步
  4. k=n/2k = \lfloor n/2 \rfloor,那么xx就是中位数

Partition\text{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{nk,k})+O(n) T(n) = T(\max \{ n-k, k\}) + O(n)

我们假设选取主元是随机的,并且每个元素有相等的概率(即P=1/nP = 1/n)被选为主元,那么对上式取期望

E[T(n)]=E[T(max{nk,k})+O(n)]=E[T(max{nk,k})]+O(n)=1ni=0n1E[T(max{nk,k})]+O(n) \begin{aligned} E[T(n)] & = E[T(\max \{ n-k, k\}) + O(n)] \\ &= E[T(\max \{ n-k, k\})] + O(n) \\ &= \frac{1}{n} \sum_{i=0}^{n-1}E[T(\max \{ n-k, k\})] + O(n) \end{aligned}

注意到,当k<n/2k < \lfloor n/2 \rfloor时,nkkn-k \ge k,否则knkk \ge n-k,那么在上面的那个合式中,T(n/2+1),T(n/2+2),,T(n1)T(\lfloor n/2 \rfloor + 1), T(\lfloor n/2 \rfloor + 2) , \dots, T(n-1)出现了两次,所以得到

E[T(n)]=1ni=0n1E[T(max{nk,k})]+O(n)2ni=n/2nE[T(i)]+O(n) \begin{aligned} E[T(n)] &= \frac{1}{n} \sum_{i=0}^{n-1}E[T(\max \{ n-k, k\})] + O(n) \\ &\le \frac{2}{n} \sum_{i=\lfloor n/2\rfloor}^{n}E[T(i)] + O(n) \end{aligned}

我们假设存在n0n_0,使n<n0n < n_0时有E[T(n)]=O(1)E[T(n)] = O(1)

那么n=n0n = n_0时,有

E[T(n)]=O(n) E[T(n)] = O(n)

n0n_0的存在是合理的,规模很小的时候,可以视为O(1)O(1)。因为递归到最后,肯定有个终止条件。比如说在这个算法中,分块的区间等于1的时候,我们可以立即得到结果。

因此我们有理由假设一直到n1n_1,都存在cc使得E[T(n)]cnE[T(n)] \le cn,当n>n1n > n_1时,把上式带入递归式

E[T(n)]2cn(n/2+n)n/22+O(n)=O(n) E[T(n)] \le \frac{2c}{n} \frac{ (\lfloor n/2 \rfloor + n )\lfloor n/2 \rfloor }{2} + O(n) = O(n)

由此证明了E[T(n)]=O(n)E[T(n)] = O(n)

不过这个算法最坏情况是O(n2)O(n^2)的。

考虑一直选取最小或者最大的元素作为主元的情况。

算法1a

虽然说算法1实现起来并不困难,但是最坏时间有时候会退化到O(n2)O(n^2),其实我们完全有办法改进选取主元的方式,使之最坏情况下时间复杂度是O(n)O(n)

改进后的选取主元的步骤:

  1. 将数组切分成n/5\lfloor n/5 \rfloor个长度为5的数组(如果最后一组不足5个元素,就丢弃掉这组,所以是向下取整)
  2. 找出每个切分后的组的中位数,逻辑上构成一个新的数组BB
  3. 调用本算法,找到中位数数组的中位数,将其作为算法1第一步的主元

在数组BB的中位数大于至多7n/10\lfloor 7n/10 \rfloor个元素,小于至多7n/10\lfloor 7n/10 \rfloor个元素。那么算法1的递归式可以改成。

T(n)T(7n10)+O(n) T(n) \le T\left( \frac{7n}{10} \right) + O(n)

同样假设有T(n)cnT(n) \le cn,代回即可验证这一假设是正确的

T(n)7cn10+O(n)=O(n) T(n) \le \frac{7cn}{10} + O(n) = O(n)