像合并排序一样,快速排序也是基于分治策略。下面是对一个典型子数组A[p..r]快速排序的分治过程的三个步骤:
分解:将数组A[p..r]分解成两个(可能为空)子数组A[p..q-1]和A[q+1..r],使得A[p..q-1]中所有的元素都小于等于A[q],而A[q+1..r]中的所有元素都大于A[q]。下标q也在该过程中计算得到。(本过程有两个步骤:确定中枢轴和中枢轴最终的位置q,使得q之前的元素都小于等于中枢轴,q之后的元素都大于中枢轴,——因此q为中枢轴的最终位置);
解决:通过递归调用快速排序,对子数组A[p..q-1]和A[q+1..r]排序;
合并:因为两个子数组是就地排序的,将它们的合并不需要额外操作,整个数组A[p..r]已排序。
下面的过程实现了快速排序:
QUICKSORT(A, p, r)
if p < r
then q = PARTIITON(A, p, r)
QUICKSORT(A, p, q-1)
QUICKSORT(A, q+1, r)
为排序一个完整的数组A,最初的调用是QUICKSORT(A, 1, length(A)),这里假定数组的下标从1开始。
快速排序的关键是数组划分过程——PARTITION,它对应分治中分解步骤。下面展示PARTITION的几种不同实现方式。(有时,为了行文方便,将PARTITION内联到QUICKSORT过程内。)
一:以左边的元素为中枢轴,从左到右扫描
代码如下:
PARTIITON1 (A, p, r)
q = p
for i = [p+1, r]
/*循-环不变式为a:A[p+1..q] < A[p]
&& A[q+1..i-1] >= A[p] */
if(A[i] < A[p])
swap(++q, i)
swap(p, q)
//A[p..q-1] < A[q] <= A[q+1..r]
循环不变式如下所示:
循环终止时得到:

交换A[p]和A[q]得到:

至此,数组以第一个元素为中枢轴,分为两个子数组A[p..q-1]和A[q+1..r],而中枢轴的最终位置确定为q。
注:《算法导论》中采用最右端的元素为中枢轴,从左往右扫描。
二,以最左边的元素为中枢轴,从右到左扫描
数组划分过程如下:
PARTIITON2 (A, p, r)
q = r + 1
for (i = u; p <= i; i--)
/*循环不变式为:A[i+1..q-1] < A[p]
&& A[q..r] >= A[p] */
if(t <= A[i])
swap(--q, i)
//A[p..q-1] < A[q] <= A[q+1..r]
循环不变式如下所示:

与方式一对比:这里从右到左扫描,且在循环不变式的过程中交换A[p]到它最终的位置。
将t作为岗哨我们可以在循环中去掉一个判断,代码如下:
PARTIITON3 (A, p, r)
q = i = r + 1
do
while A[--i] < t
//空循环,遇到大于等于t的元素则停止
;
//将大于t的元素放到A[q..r]中
swap(--q, i)
while r < q
//A[p..q-1] < A[q] <= A[q+1..r]
三、从两侧逼近
考虑这样一种异常情况:数组由n个相等的元素构成。这种情况下,插入排序表现很好——每个元素都在它最终的位置上,故只需要O(n)的时间;而PARTIITON1表现糟糕——需要运行n-1次PARTIITON过程,每次PARTIITON都只能剥离第一个元素,且要花费O(n)的时间,故一共需要O(n2)的时间。
我们可以通过两侧逼近,来避免这种情况。循环不变式如下所示:

下标i和j初始化为数组的两个边界极点。主循环中包含两个循环:第一个循环,i从左往右移动过比t小的元素,遇到比t大的元素则停止。第二个循环,j从右往左移动过比t大的元素,遇到比t小的元素则停止。主循环测试i、j是否已经交叉了,如果没交叉则交换i、j,继续循环。
代码如下:
void QUICKSORT4(A, p, r)
if (r <= p)
return
t = A[p]
i = p
j = r + 1
while true
do
i++
while i <= r && A[i] < t
do
j--
while t < A[j]
//有交叉则退出主循环
if i > j
break
//交换i,j使得循环不变式满足
swap(i, j)
//j指示的位置为t最终的位置
swap(p, j)
//A[p..j-1] < A[j] <= A[j+1..r]
QUICKSORT4(A, p, j-1)
QUICKSORT4(A, j+1, r)
从上面的代码可以看出:当数组有n个相等的元素时,数据划分部分进行了多次的交换,虽然交换次数增多了,但j的位置尽可能的靠近中间位置。不会再出现一个子数组包含0个元素,另一个子数组包含n-1个元素的情况(PARTIITON1会造成这种情况),时间复杂度接近O(nlgn)。
四、随机挑选中枢轴
目前为止,我们都是拿第一个元素作为中枢轴。考虑这种情况:数组已经排序了,如果仍然拿第一个元素作中枢轴,数组划分将围绕最小的元素、次小的元素,如此直到最后一个元素,时间复杂度接近O(n2)。如果我们随机挑选一个元素作为中枢轴,情况就会好转。我们可以通过交换A[p]与A[p..r]中的某个随机元素(代码其余部分不变)来达到以随机元素为中枢轴的目的。交换的代码示意如下:
swap(p, randint(p, r))
五、“聪明”的快速排序
在规模较小的数组上进行快速排序的效率是很低的(只有当数组规模较大时,快速排序的优势才体现出来),远不如插入排序的效率。为此,Bob Sedgewick发明了一种聪明的快速排序方法——当数组规模较小时,什么都不做。我们通过改变代码中的第一条语句来实现这个想法,如下:
if r – p < cutoff
return
这里,cutoff是一个小整数,经验值为50。
当程序结束时,数组虽然没被完全排序,但是数组被分成很多规模较小的块,块内元素的顺序是随机的,但块与块之间的是有序的——块中元素都比左边块中的元素大,比右边块中的元素小。整个数组是几乎有序的,再调用插入排序就可以将整个数组排序。对整个数组的排序代码如下:
QUICKSORT5(A, 1, n)
//调用快速排序之后数组Á被分解成许多有序À的Ì小块,再调用插入排序使整个数组有序
InsertSort3()
InsertSort3的实现请参考链接点击打开链接点击打开链接
当然,也可以在每个小块内部调用插入排序来使得小块内部元素有序。结合以上情况,得到QUICKSORT5,代码如下:
//改进后的插入排序
void INSERTSORT(A, p, r)
for i = [p + 1, r]
t = A[i]
for (j = i; j > p && A[j - 1] > t; j--)
A[j] = A[j -1]
A[j] = t
//产生[p,r]内的随机下标
int RANDINT(p, r)
return p + rand() % (r - p + 1)
//快速排序
void QUICKSORT5(A, p, r)
if (r - p < cutoff)
//也可以把插入排序移到快速排序之外,在那里插入排序从数组的第一个元素开始插入排序
INSERTSORT(A, p, r)
return
swap(p, RANDINT(p, r))
t = A[p]
i = p
j = r + 1
while true
do
i++
while i <= r && A[i] < t
do
j--
while t < A[j]
//有交叉则退出主循环
if i > j
break
//交换i,j使得循环不变式满足
swap(i, j)
//j指示的位置为t最终的位置
swap(p, j)
//A[p..j-1] < A[j] <= A[j+1..r]
QUICKSORT5(A, p, j-1)
QUICKSORT5(A, j+1, r)