- 作为最常用和好用的排序方法, 快速排序是每个工程师必须随时能够手写出代码和解释其运行原理的算法
快速排序算法
- Quick-Sort(A, p, r)
if p<r
q = Partition(A, p, r)
Quick-Sort(A, p, q-1)
Quick-Sort(A. q+1, r)
- Partition(A, p, r)
q = RANDOM(p, r)
swap A[q] and A[r] //现在A[r]是一个随机挑选的值
i = p-1 // i指向分界线左侧最后一个元素, 其初始值定在p-1表示还没有任何小于q的元素
for j=p to r-1 //区分已经分好的未分好的区域的指针不断向右移动
if A[j]<=A[r]
i++ //让小于q的分界线指针向右移动
swap A[i] with A[j] //发现A[j]是比q所在值小的话, 把A[j]换到分界线的左侧第一个位置, 即A[i]位置
swap A[i+1] and A[r] //交换分界线右侧第一个位置的元素, 保证q左侧的<=q, q右侧的>=q
return q
快排时间复杂度分析
CLRS告诉我们, 快速排序在不断递归的过程中, 递归树的每一层花费的时间都是Partition的时间, 因此整体的时间复杂度T(n)取决于整个算法到底进行了多少次Partition, 而每次Partition的时间复杂度又是不相等的 -- Partition的时间复杂度取决于其中的for循环进行的次数, 而 for循环主要的操作就是比较大小.
所以, 整个算法的T(n)最后在研究进行了几次比较大小操作的问题
-
对于输入数组A的规模为n的情况, 我们已经知道的:
- 每个元素对(pair)最多只比较一次, 而且这次比较只出现在pair中有一个元素被选中为q (A[q] becomes partition pivot).
- 反过来说, 在选q的时候, 如果一个pair<ai, aj> (i<j且都∈[current-p, current-r])中的元素都没被选中, 而是有一个i<=x<=j的的元素被选为pivot, 那么ai和aj将永远失去相互比较的机会. 注意, 此处如果[i, j]区间外的元素被选中, 由于a[i], a[j]可能被分配到同一边, 仍存在相互比较的机会, 这对写概率公式很重要
- Ind{[i, j]区间中A[i], A[j]外的其他元素不被选中} = Ind{a[i]或者a[j]被选中作为pivot} == Ind{在[i, j]中i被选中} + Ind{在[i, j]中j被选中}
- E[Ind{在[i, j]中i被选中}] = E[Ind{在[i, j]中j被选中}] = 1/(j-i+1)
- 规模为n的情况下, 且元素对不考虑排序, 会有C(n, 2)= (n(n-1))/(21) 个pair的组合, 一般为了算期望, 我们会改写成 Σ[in-1]Σ[j=i+1n] 形式 (容易证明得到这两个写法是完全等价的) 来遍历得出所有可能的组合, 即pair的数目
假设X为总的比较次数的随机变量 E[X] = Σ[in-1]Σ[j=i+1n] (Ind{a[i]或者a[j]被选中作为pivot}) = Σ[in-1]Σ[j=i+1n](2 / (j-i+1) ) = O(nlgn)