快速排序原理
像归并排序一样,快速排序也是一种分治的递归排序.它是指从待排序数组中选取枢纽元,遍历数组,将小于枢纽元的元素放入S1,大于枢纽元的元素放入S2,然后再对S1和S2进行递归的调用,最后返回的结果是排序后的数组.
快速排序策略
举一个经典快速排序的例子,将数组S排序的基本算法由下列四步组成:
1.如果S中元素个数是0或者1,则返回.
2.取S中任一元素v,称之为枢纽元.
3.将(S中其余元素)划分成两个不相交的集合: S1=和S2=.
4.返回后跟v,继而返回
选取枢纽元-三数中值分割法
一般来说不推荐直接选取第一个元素作为枢纽元,一种安全的方法是随机选取,更好的方案是根据三数中值分割法选取枢纽元.一组N个数的中值(也称中位数)是第[N/2]个最大的数.但是这会明显减慢快速排序的速度.因此一般的做法是使用最左端,最右端和中心位置上的三个元素的中值作为枢纽元.
分割策略
下面这种算法已经被证明能给出好的结果.该算法的第一步是通过将枢纽元与最后的元素交换使得枢纽元离开要被分割的数据段.i从第一个元素开始而j从倒数第二个元素开始.
例如现在有一个数组8,1,4,9,6,3,5,2,7,0.根据三数中值分割法找到右端,左端,中心位置分别为0,8,6,所以这个数组的枢纽元为6.按照分割策略的第一步如下:
暂时假设所有的元素互异.在分割阶段要做的就是把所有小元素移到数组的左边而把所有大元素移到数组的右边.'大'和'小'相对于枢纽元而言.
当i在j的左边是,我们将i右移,移过那些小于枢纽元的元素,并将j左移,移过那些大于枢纽元的元素.当i和j停止时,i指向一个大元素而j指向一个小元素.如果i在j的左边,那么将这两个元素互换,其效果是把一个大元素推向右边而把一个小元素推向左边.在上面的例子中,i不移动,而j滑过一个位置,情况如下图:
此时满足条件i指向大于枢纽元的元素而j指向小于枢纽元的元素,并且i在j的左边,所以交换i和j所指向的元素如下:
重复此过程知道i和j彼此交错为止:
此时,i和j已经交错,故不再交换.分割的最后一步是将枢纽元与i所指的元素交换.
在最后一步当枢纽元与i所指向的元素交换时,我们知道p<i位置的元素都小于枢纽元,而p>i上的元素都大于枢纽元.
现在我们来讨论如果数组中出现与枢纽元相同的元素的情况.讨论的关键点在于当我们遇到与枢纽元相同的元素时,i是否应该停止下来进行交换,或是不需要停止继续遍历.直观地看,i和j应该做相同的工作,否则分割将出现偏向一方的倾斜.例如,如果i停止而j不停,那么所有等于枢纽元的元素都将被分配到S2中.
假设数组中所有元素都相等,如果i和j都停止,那么在相等的元素间将进行多次交换,虽然这并没有什么意义.但是其正面效果则是i和j将在中间交错.因此当枢纽元被替代时,这种分割就会建立两个几乎相等的子数组.归并排序的分析告诉我们,此时总的运行时间将会是.
如果i和j都不停止,那么就应该有相应的程序防止i和j越出数组的端点,不进行交换的操作.虽然这样似乎不错,但是正确的实现方法却要把枢纽元交换到i最后到过的位置,这个位置是倒数第二个位置(或是最后一个位置,这依赖于精确实现).这样的作法将会产生两个非常不均匀的子数组.如果所有的关键字都相同,那么运行的时间将会是.
综上所诉,当我们在遍历数组时,如果遇到了与枢纽元相同的元素,那么我们还是让i和j停下来老老实实交换位置的好.
快速排序代码实现
public static void quickSort(int[] a){
quickSort(a,0,a.length-1);
}
public static void quickSort(int[] a,int left, int right){
if(left+CUTOFF<=right){
int pivot = median3(a, left, right);
int i = left; int j = right - 1;
for(;;){
while(a[++i]<pivot){}
while(a[--j]>pivot){}
if(i<j)
swapReference(a,i,j);
else
break;
}
swapReference(a,i,right-1);
quickSort(a,left,i-1);
quickSort(a,i+1,right);
}else{
insertSort(a,left,right);
}
}
public static int median3(int[] a,int left, int right){
int center = (left + right)/2;
if(a[center]<a[left])
swapReference(a, left, center);
if(a[right]<a[left])
swapReference(a, left, right);
if(a[right]<a[center])
swapReference(a, center, right);
swapReference(a, center, right-1);
return a[right-1];
}
public static void swapReference(int[] a, int top,int bottom){
int tmp = a[top];
a[top] = a[bottom];
a[bottom] = tmp;
}
public static void insertSort(int[] arr,int left , int right){
int j;
for(int p = 1;p<arr.length;p++){
int temp = arr[p];
for(j=p;j>0&&temp<arr[j-1];j--){
arr[j] =arr[j-1];
}
arr[j] = temp;
}
}
这段代码包括划分和递归调用.有几件事值得注意,在方法中将i和j初始化为比他们的正确位置超过1个位置,使得不存在特殊情况需要考虑.此处的初始化依赖于三数中值分割法有一些副作用的事实.
注意CUTOFF为子数组的个数,在分割子数组时如果数组的个数小于3(即CUTOFF<3)会出现错误.一般设置为CUTOFF=10,当数组元素小于10个的时候,实践证明插入排序具有更好的时间复杂度.
时间复杂度
快速排序在最坏的情况下时间复杂度为.最好的情况下时间复杂度为.平均情况下时间复杂度为.