快速排序介绍
快速排序像归并排序一样,快速排序也是一种分治的递归算法。将数组S排序的基本算法由下列简单的四步组成:
- 如果S中元素个数是0或1,则返回。
- 取S中任一元素做为枢纽元。
- 然后将比枢轴元大的数组元素放在枢轴元的右边,比枢轴元小的数组元素都放在枢轴元素的左边。
- 再分别对枢轴元左边的序列和枢轴元右边的序列进行快速排序。
选取枢纽元
现在翻看网上资料,最多的就是把序列的第一个元素用作枢纽元。如果输入是随机的,那么这时可以接受的,而如果输入是预排序或者是反序的,那么这样的枢纽元就产生一个劣质的分割,因为所有的元素不是被划入S1就是被划入S2.更糟糕的是,这种情况毫无例外的发生在所有的递归调用中。因此,使用第一个元素做为枢纽元是绝对可怕的坏主意。
一种安全的方针是随机选取枢纽元。一般来说这种策略非常安全,除非随机数发生器有问题,因为随机的枢纽元不可能总在接连不断的产生劣质的分割。
另外还有一种就是三数中值分割法。枢纽元的最好的选择就是数组的中值。不幸的是,这很难算出并且会明显减慢快速排序的速度。因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。
算法实现
在本文中提供了两种快速排序的实现,分别是把序列的第一个元素用作枢纽元的实现和采用三数中值分割法作为枢纽元的代码实现。
首先我们来看第一种,假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。那么此时数字6作为枢纽元。算法描述如下图所示
代码如下
public static void main(String [] args){
int[] a={6,1,2,7,9,3,4,5,10,8};
quicksort( a,0,a.length-1);
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
private static void quicksort(int[] a, int left, int right) {
if(left<right){
int i=left;
int j=right;
//存放枢纽元
int temp=a[left];
for(;;){
//先从右边往左找
while(a[j]>=temp&&i<j){j--;}
//再从左往右找
while(a[i]<=temp&&i<j){i++;}
//交换
if(i<j){
int t = a[i];
a[i]=a[j];
a[j]=t;
}else{break;}
}
a[left]=a[i];
a[i]=temp;
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
}
接下来我们再来看看利用三数中值法来选取枢纽元的算法实现。采用三数取中法时,会有一个问题,就是当数组不断的递归划分变小之后,枢轴左边的元素个数不足3,这样三数取中法就失去了应有的意义。此外,正如前面提到,对于小数组而言,插入排序反而比快速排序的效率更高。正是基于以上两个原因,我们可以将快速排序与插入排序结合。当递归划分的数组变小之后,达到某个值(CUTOFF)时,采用插入排序。
private static final int CUTOFF = 10;
static int[] a = {6,1,2,7,9,3,4,5,10,8,24,30,25,39,16,14};
public static void main(String[] args){
quickSort(a, 0, a.length - 1);
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
/**
* 快排的基本操作:通过三数取中法来选取枢轴元素
*
* @param arr
* 在[left, right]之间选择pivot element
* @param left
* index of arr to chose pivot element
* @param right
* index of arr to chose pivot element
* @return pivot element
*/
private static int media3(int[] arr, int left, int right) {
int center = (left + right) / 2;
if (arr[center]<arr[left])
swapReference(arr, center, left);
if (arr[right]<arr[left])
swapReference(arr, right, left);
// 参考前面两个if比较之后,最小的元素被放置在
// arr[left],然后下面再比较中间与最右的元素,将最大的元素放在arr[right],而arr[center]存放中间元素(pivot)
if (arr[right]<arr[center])
swapReference(arr, right, center);
swapReference(arr, center, right - 1);//将枢轴元素放在 arr[right-1]上.便于快排交换元素
return arr[right - 1];
}
private static void swapReference(int[] arr, int from, int to) {
int tmp = arr[from];
arr[from] = arr[to];
arr[to] = tmp;
}
/**
* 实现了递归的快排主例程, internal quicksort method that makes recrusive calls
*
*/
private static void quickSort(int[] arr, int left, int right) {
if(left + CUTOFF <= right)
{
int pivot = media3(arr, left, right);
//begin partitoning
int i = left, j = right - 1;//在media3中已经将比pivot小的元素放到了a[left]上,把pivot放到了arr[right-1]上,故下面的while中是 ++i 和 --j
for(;;)
{
while(arr[++i]<pivot){}
while(arr[--j]>pivot){}
if(i < j)
swapReference(arr, i, j);
else
break;
}
swapReference(arr, i, right - 1);//restore pivot
quickSort(arr, left, i - 1);
quickSort(arr, i + 1, right);
}else
//Do an insertion sort on the subarray
insertSort(arr, left, right);
}
private static void insertSort(int[] a, int left ,int right) {
for (int p = left + 1; p <= right; p++) {
int tmp = a[p];//保存当前位置p的元素,其中[0,p-1]已经有序
int j;
for (j = p; j > 0 && tmp<a[j - 1]; j--) {
a[j] = a[j - 1];//后移一位
}
a[j] = tmp;//插入到合适的位置
}
}
快速排序的时间复杂度
平均情况下,时间复杂度为O(NlogN),最坏情况下为O(N^2)