- 正如它的名字一样,快速排序实在实践中最快的已知排序算法,它的平均运行时间是O(N log
N)。快速排序的基本思想就是选取一个枢纽元,将所有小于它的元素都放在它的左边,所有大于它的元素都放在它的右边。此时此元素位于它的最终位置。然后对左边和右边的元素执行相同的操作。
就其对S这个数组的具体操作步骤来说,可以分为下列简单的四步:
1.如果S中的元素个数是0或1个,则返回(0个或1个元素可是为有序)
2.去S中的任意一个元素v,此元素为枢纽元
3.将S中除了v的元素分为两部分,S1与S2,S1的所有元素均小于等于v,S2的所有元素均大于等于v
4.对S1和S2继续执行快速排序
由于第3步中分割的情况会由于v的选取方法不同而不同。不难看出,一种比较好的情况是S1和S2的元素个数差不多。
其中,一种没有经过充分考虑的选择是将数组的第一个元素当做枢纽元。如果输入时随机的,那么这么做是可以接受的。但如果数组是预排序或者反序,那么这样的枢纽元就会导致劣质的分割。不难看出,元素不是被分到S1就是被分到S2,这样快速排序所花费的时间将是二次的。
首先,我们还是选取第一个元素当做枢纽元,我们这里还有一个问题没有解决,就是如何将元素划分到S1和S2, 虽然选取第一个元素是种糟糕的选择,但这也是一种很常见的方案,而且只要稍作修改便可以避免糟糕的情况。
这里,我不得不使用图示的办法,如何操作很难用文字解释清楚,然而画图总是很麻烦的。。。
- 我们让i指向第二个元素,j指向最后一个元素。首先将i右移,直到指向第一个比枢纽元大的数,只有j左移,直到指向第一个比枢纽元小的数,此时i和j指向的两个元素的位置交换。继续以上操作,直到i=j或者i>j(此时不交换元素)。这时不难发现,j右边的元素均大于枢纽元,而j所指向的元素小于枢纽元,此时交换数组第一个元素和j所指向的元素。然后对j左边的元素继续进行该操作,对j右边的也是如此(注意不包括j,j指向的位置就是元素最终的位置)。
下面给一个程序例程:
static inline void swap(int *a,int *b)
{
int tmp;
tmp=*b;
*b=*a;
*a=tmp;
}
void quick(int a[],int L,int R)
{
int i,j;
int pivot;
if(L<R)
{
pivot=a[L];
i=L+1;j=R;
for( ; ; )
{
for(;a[i]<pivot&&i<=R;i++){}
for(;a[j]>pivot;j--){}
if(i<j)
swap(&a[i],&a[j]);
else
break;
}
swap(&a[L],&a[j]);
quick(a,L,j-1);
quick(a,j+1,R);
}
}
void QuickSort(int a[],int lenth)
{
quick(a,0,lenth-1);
}
这里有个值得注意的地方,就是i最后可能会出现越界。不难想象,如果枢纽元是最大值的话,那么i肯定会越界。所以这里还要限定i的范围。而j却不需要,因为j一旦指向最左边的枢纽元便会停下,而且j之后也不会在左移,此时i和j必定交错。
- 事实上,枢纽元最好的选择便是数组的中值,因为这样S1和S2的元素总是一样的。然而实际操作中,我们不可能去寻找数组的中值,这显然会使快速排序的速度放慢。另一种做法是随机选取枢纽元。而随机数的生成显然是很昂贵的。
一般的做法我们是选取数组最左端,最右端,中间三个数的中值作为枢纽元。这种做法可以消除预排序这种坏情况。这里给出例程:
static inline void swap(int *a,int *b)
{
int tmp;
tmp=*b;
*b=*a;
*a=tmp;
}
int Media(int a[],int L,int R)
{
int Center;
Center=(L+R)/2;
if(a[L]>a[Center])
swap(&a[L],&a[Center]);
if(a[L]>a[R])
swap(&a[L],&a[R]);
if(a[Center]>a[R])
swap(&a[Center],&a[R]);
swap(&a[L],&a[Center]);
return a[L];
}
void quick(int a[],int L,int R)
{
int i,j;
int pivot;
if(L<R)
{
pivot=Media(a,L,R);
i=L+1;j=R;
for( ; ; )
{
for(;a[i]<pivot;i++){}
for(;a[j]>pivot;j--){}
if(i<j)
swap(&a[i],&a[j]);
else
break;
}
swap(&a[L],&a[j]);
quick(a,L,j-1);
quick(a,j+1,R);
}
}
void QuickSort(int a[],int lenth)
{
quick(a,0,lenth-1);
}
这里使用了Media函数。它的做法便是找出左端,右端,中间这三个数中的中值,并把它放在数组的第一个位置。正因为如此做,我们前一个例程只需稍作修改即可。而且不难看出,Media函数将最大值放在了数组的最右端,这时可以不对i进行越界判断,因为i指向最大值时必定会停下。
- 之前讨论过插入排序,事实上,对于小数组(一般长度不超过20个元素),快速排序不如插入排序好。而且,由于这里的快速排序是递归的,而调用函数的开销是很大的。通常的解决方案是对于小的数组,我们使用插入排序。使用这种策略大约可以节省15%的时间。一种好的截止范围是数组元素个数等于10的时候。这种做法也避免了一些有害的情况。在使用Media时,元素只有一个或两个的情况。这里再给一个例程:
void InsertSort(int arr[],int lenth)
{
int p,i;
int tmp;
for(p=1;p<lenth;p++)
{
tmp=arr[p];
for(i=p;i>0&&arr[i-1]>tmp;i--)
arr[i]=arr[i-1];
arr[i]=tmp;
}
}
static inline void swap(int *a,int *b)
{
int tmp;
tmp=*b;
*b=*a;
*a=tmp;
}
int Media(int a[],int L,int R)
{
int Center;
Center=(L+R)/2;
if(a[L]>a[Center])
swap(&a[L],&a[Center]);
if(a[L]>a[R])
swap(&a[L],&a[R]);
if(a[Center]>a[R])
swap(&a[Center],&a[R]);
swap(&a[L],&a[Center]);
return a[L];
}
#define Cutoff (10)
void quick(int a[],int L,int R)
{
int i,j;
int pivot;
if(L+Cutoff<=R)
{
pivot=Media(a,L,R);
i=L+1;j=R;
for( ; ; )
{
for(;a[i]<pivot;i++){}
for(;a[j]>pivot;j--){}
if(i<j)
swap(&a[i],&a[j]);
else
break;
}
swap(&a[L],&a[j]);
quick(a,L,j-1);
quick(a,j+1,R);
}
else
InsertSort(a+L,R-L+1);
}
void QuickSort(int a[],int lenth)
{
quick(a,0,lenth-1);
}