快速排序,是一种基于分治思想的排序算法。分治,简单来说就是“大事化小”,从而达到最基本的问题,然后再将其合一。
在快速排序算法中,每次需要把一个数组A[p...r]分为三部分:
A[p...q-1] A[q] A[q+1...r]
而对两个子数组:A[p...q-1]和A[q+1...r]的要求却很少:只需要满足A[p...q-1]中的每个元素都小于等于A[q],A[q+1...r] 中的每个元素都大于等于A[q]即可。每个子数组可以为空,内部也不需要有序。之后,再将两个子数组进行这样的划分,如此递归下去,最终得到的就是一个有序的数组。
该算法中,最重要的步骤就是数组的划分。用函数 partition 来实现:
传入参数:待排序数组A、当前子数组的首尾位置p和r
返回值:中间元素位置q
开始时,选取当前数组的最后一个元素作为主元(pivot element),当然,这样选择完全是个人定的,其实完全可以选择该数组中的任意一个元素。选定了主元之后,设定一个游标,然后依次遍历整个数组,每次比较当前元素和主元。把小于主元的元素放在游标左边,大于等于主元的元素放在右边。最后再把主元放在游标的位置即可,而且游标的下一个位置就是要返回的中间元素位置q。
如图所示,在给定的数组A[p...r]中,主元是最后一个位置的元素A[r]=x,游标是i,j表示当前正遍历到的位置。我们可以看到,从p到i之间是小于等于x的元素,从i+1到j-1之间是大于x的元素。
若当遍历到的下一个元素小于等于x时,游标i右移一位,即腾出一个位置给该元素,当然j也要右移。若当遍历到的下一个元素大于x时就不需要移动游标,只需j右移即可。
算法代码如下:
#include <iostream>
using namespace std;
void swap( int *a, int *b )
{
int temp = *a;
*a = *b;
*b = temp;
}
int partition( int *A, int front, int behind )
{
int i = front - 1;
for( int j=front; j<behind; j++ )
{
if( A[j] <= A[behind] )
{
i++;
swap( &A[i], &A[j] );
}
}
swap( &A[i+1], &A[behind] );
return i + 1;
}
void QuickSort( int *A, int front, int behind )
{
int mid;
if( front < behind )
{
mid = partition( A, front, behind );
QuickSort( A, front, mid-1 );
QuickSort( A, mid+1, behind );
}
}
int main()
{
int A[10] = {2,5,3,1,0,9,8,6,7,4};
QuickSort( A, 0, 9 );
for( int i=0; i<10; i++ )
cout << i[A] << " ";
cout << endl;
return 0;
}
关于快速排序的性能也是值得分析的。快速排序的运行时间依赖于划分是否平衡,就是划分的两个子数组中元素的个数是否相近。很容易直观的感觉到,最好的情况就是每次都是均分,这样的时间复杂度就是O(nlgn)。而最坏的情况就是每次划分的子数组中都有一个为空,最典型的就是把一个升序的数组排成降序。这样的话每次划分都只能排好一个元素,所以此时快速排序就退化为插入排序,时间复杂度就是O(n²)。
一般来说,如果把快速排序的划分平均来看,可以得到一棵递归树。如果我们假设划分成的两个子数组中元素个数的比例为一个常数,比如1:1,或者99:1,那么递归树的深度就为Θ(lgn),而其中的每一层的时间代价都是O(n),因此算法的运行时间总是O(nlgn)。