在实践中,需要对大量数据排序时,快速排序常常是最佳的选择。快速排序是基于分治思想的一种算法,和归并排序有相似之处。在最坏情况下,快速排序的时间复杂度是O( n^2 );平均情况下,快速排序的时间复杂度是 O( n * lg n)。我们知道归并排序的时间复杂度是O( n * lg n),那么为什么在实践中会发现快速排序常常要优于归并排序呢?通过分析快速算法,我们可以发现最坏情况出现的概率极为小;而在平均情况下,由于快速排序省去了分治思想中的合并过程,从而节省了时间。这是它能够优于归并排序的主要原因。另外,快速排序不需要额外的空间,具有空间复杂度优势。
1. 算法导论中给出的快速排序算法的主要思想是
(1) 假设要对数组 A[ p : r] 以升序排列,我们可以待排序数组中最后一个元素A[r] 作为基准数。
(2) 借助一个标记 i,使它初始状态下,指向数组的第一个元素之前,i 左边没有元素,我们认为此时 i 左边的元素都比基准数小。在之后的过程中,保证 i 左边的元素都不大于基准数,也是使算法正确的必要条件。
(3) 利用指针 j 从第一个元素开始扫描数组,遇到小于或等于基准数的元素时,就将 i加 1,然后交换该元素与此时 i 指向的元素,这样就保证了 i 左边的元素都不大于基准数。遇到大于基准数的元素就保持 i 指向不变,继续扫描,这样同时保证了 i 右边 j 左边的的元素都比基准数大。扫描完基准数前一个元素时,终止扫描,并将此时 i+1 指向的元素与 A[r] 互换。这样之后,我们就发现A[i+1] 左边的元素都不大于 A[i+1],而 A[i+1] 右边的元素都不小于 A[i+1],从而以A[i+1] 为中心点把 A[ p : r] 分成了两部分。
(4) 分别对上一步划分出的两部分(A[ p : i])和(A[i+2 : r])重复上述三步操作(递归),直到待排序子数组的起始下标小于等于终止下标。全部完成后,就得到了排好序的新数组。
下图表现了最开始的一次递归过程。
C 语言实现快速排序算法的代码如下。
int partition(int *src, int st, int ed){
int i = st - 1;
int j = st;
int key = src[ed];
int tmp = 0;
for(; j < ed; ++j){
if(src[j] <= key){
++i;
tmp = src[i];
src[i] = src[j];
src[j] = tmp;
}
}
tmp = src[i+1];
src[i+1] = key;
src[ed] = tmp;
return i+1;
}
void quickSort(int *src, int st, int ed){
if(st<ed){
int md = partition(src, st, ed);
quickSort(src, st, md-1);
quickSort(src, md+1, ed);
}
}
2. 上面的例子是快速排序算法的一种平凡(trivial solution)实现,实际上,C.RA. Hoare 博士在1962年首次提出快速排序算法时,给出了非平凡的(nontrivial solution)实现。相比算法导论中给出的解法,它是更优的,这主要是因为每趟遍历过程中,它只扫描一个元素一次。理解该实现方法的要点在于由于选取中心点并将它复制给变量key 后,在待排序数组中就产生了一个空位。在之后的遍历过程中,每次向空位赋值后又产生了新的一个空位,这样可以一直利用将合适的值放入空位,然后产生新的空位,直到遍历结束。
C 语言实现该算法思想的代码如下。
void quickSort(int *src, int st, int ed){
if(st >= ed) return;
int key = src[st];
int i = st, j = ed;
while(i < j){
for(; j > i; --j){
if(src[j] < key){
src[i] = src[j];
++i;
break;
}
}
for(; i < j; ++i){
if(src[i] > key){
src[j] = src[i];
--j;
break;
}
}
}
src[i] = key;
quickSort(src, st, i-1);
quickSort(src, i+1, ed);
}