微困啊。。贴完这篇赶快睡觉去。。
OK, Let’s get started…
快速排序和上一篇归并排序一样也是使用DIVIDE AND CONQUER的策略。但不同之处是快排是就地排序的,也就是说在每一时刻只有常数个元素会存储在原序列之外,对于空间复杂度来说比Merge Sort要好。
同样的对于基于分治法策略的算法来说都有三个基本步骤:
DIVIDE:
快排的分治步骤是通过一个Partition的子程序完成的,它基于一个随机选择的pivot元素将原序列分成两个部分,其中左半边的元素均小于pivot,而右半边均大于,pivot中间位置。
CONQUER:
与归并排序一样,CONQUER的步骤都是递归的调用自身来处理两个子序列。直到满足递归终止条件,即处理到单个元素,那意味着已经是排序好的。
COMBINE:
快排没有合并步骤,因为是原地排序,不需要像Merge Sort一样把子序列合并成最终序列。
和归并排序做比较对于理解快速排序是很有帮助的,之前谈到过归并排序在递归后存在一个回代过程,递归到处理2个单元素子序列后才从递归树的底部开始向上执行Merge,直到回到顶部。而快速排序则是从递归树的顶部开始逐步排序,当到达底部后意味着整个序列已经排好序了。
其实贴个图更好描述它的过程。。不过今天太困了。。睡了先。。
代码如下,依然是python,写起来和说话一样:
def partitionOfQuickSort(L, start, end):
key = L[start]
i = start
j = i + 1
while j <= end:
if L[j] <= key:
i = i + 1
tmp = L[i]
L[i] = L[j]
L[j] = tmp
j = j + 1
L[start] = L[i]
L[i] = key
return i
def quickSort(L, start, end):
if start < end:
mid = partitionOfQuickSort(L, start, end)
quickSort(L, 0, mid - 1)
quickSort(L, mid + 1, end)
return L
这段程序中的PARTITION步骤中,每次都是将序列中的第一个数作为主元,这样对于处理已经排好序或者反向排好序的输入,代价是很大的,因为每次划分子序列并没有显著的降低输入的规模,这种情况运行时间的递归表达式为
T(n) = T(0) + T(n – 1) + cn
cn为Partition步骤的代价
所以有T(n) = Θ(n2) 对于排序来说是苦情的表达式。。
但算法导论里有提到,可以有很多种方法改进快排,比如随机选择主元的随机化快速排序,它的时间复杂度就是nlgn。而且因为是原地排序,一般情况下均比归并排序快2到3倍。
Partion的循环不变量:
在上面贴的代码中,变量i和j各维护2个子序列,其中A[1, i]是比主元小的序列,A[i+1, j]是比主元大的序列。j向右移动,每当遇到比主元小的元素就让i向右移动,然后与A[i]进行交换,这样在迭代的过程中两个表的不断向右移动直到填满整个序列,最后让A[i]与主元交换,即完成了工作。