005【算法篇】随机化快速排序及其时间复杂度
呃,本文有点长……还用到一点点概率论知识
在讲随机化之前,先说下目前大家所熟识的快速排序,先上伪代码:
PARTITION(A,p,r)
x = A[p]
i=p
for j=p+1 to r
if A[j]<=x
i=i+1
A[i]<->A[j]
A[i]<->A[p]
return i
最坏情况下的时间复杂度
我们先来分析下最坏情况下的时间复杂度。何为最坏情况?输入的数据已升序或者降序,致使每次划分的时候总有一个子数组中的元素个数为0,而另一个子数组中的元素个数为n-1,由此我们知道此时的时间复杂度为:
我们根据上一讲的递归树法进行分析,如下图所示:
故(1)式的时间复杂度计算如下:
最好情况下的时间复杂度
说完最坏情况,我们再聊最好情况,虽然我们一般不分析这种bogus。最好的情况下是什么样的呢?一般是说每次划分的主元刚好把数组一分为二,两个子数组的元素个数均为n/2,由此其时间复杂度为:
具体过程不证明了,具体可参见上一讲的内容。
综合来说,目前的快速排序其时间复杂度介于上述两种情况之间,也即:
由于输入的数据其排序程度我们难以控制,再加上实际工作中大部分情况下输入数据其实已经大致排序,所以使用常规的快速排序将趋于最坏情况下的时间复杂度,这也是我们不想看到的结果,由此下文我们将开始讨论随机化的快速排序。
改变主元位置
由于我们无法控制输入数据的排序程度,又想得到趋近于最好情况的时间复杂度,我们还有什么办法?
既然我们无法控制输入数据,那我们是不是可以尝试下控制每次划分的主元?由于常规的快速排序我们的主元要么在数组的队首或者队尾,如果我们换到其它位置,可不可行?比如针对一组随机输入数据,每次按1:9来划分数组,会有怎么样的时间复杂度?
一般我们的思路是先大胆假设,然后小心论证,如果论证结果确实符合预期,那我们就可以对代码进行改造。话不多说,按刚刚的1:9来划分数组,我们将得到这样一个时间复杂度:
这种情况下无法直接使用主定理法,所以我们还是用老朋友递归树法画图如下:
由此可知:
发现了吗?最坏情况下的时间复杂度开始趋近于𝛰(nlgn)了!由此可以证明,针对随机化的输入数据,改变主元位置能够显著影响时间复杂度。
随机化快速排序
借由上文我们已经很清楚改变主元位置对时间复杂度的影响,那么我们是不是固定一个固定比例的划分就可以了?答案不是的,因为输入的数据存在一定随机性,我们不能保证每次用户提供的数据都是随机排序的,那么第一次按固定比例的划分假设是平衡的,不保证以后每次递归后的划分是平衡的,由此为了在各种输入场景下均保持良好的时间复杂度,我们想到了随机主元位置。
所谓随机主元位置,也即每次划分时的位置随机性选择A[1..n]中的某个位置k,将数组随机机划分为A[1..k-1]和A[k+1..n]两部分。
由于主元位置是随机的,也即每次主元位置的概率是相等的,数理统计中称x~B(0,1),也即0-1分布,由此主元刚好划分到k时的数学期望为:
当主元位置随机之后,时间复杂度如何求解?如何验证确实针对任何输入数据情况下,均能有较好的时间复杂度?
当主元位置介于[1..n]之间时,共有n种划分方式,由此我们可以写出随机化下的时间复杂度:
由于k的位置是随机的,所以时间复杂度也是随机的,加上随机的机率相同,那么我们只需要利用概率论中的数学期望进行求解即可。
我们对(5)式进一步利用数学期望进行求解,可得如下过程:
式(6)最后一步的过程比较艰辛,我不是数学专业的,所以也搞不清楚计算过程,反正就这个么答案……感兴趣的童鞋可以利用上一讲的代换法进行验证(可能要用到微积分,我就不写了)。
由此可知,针对任何输入数组,无论其排序程度如何,随机化的快速排序的时间复杂度为𝛰(cnlgn),趋近于最好情况下的时间复杂度,据说至少三倍速度于常规的快速排序。
最后奉上随机化快速排序的伪代码如下:
RANDOMIZED_PARTITION(A,p,r)
i=random(p,r)
A[p]<->A[i]
return PARTITION(A,p,r)
RANDOMIZED_QUICKSORT(A,p,r)
if p<r
q=RANDOMIZED_PARTITION(A,p,r)
RANDOMIZED_QUICKSORT(A,p,q-1)
RANDOMIZED_QUICKSORT(A,q+1,r)