1 基本思想
排序的基本操作是比较2个数,比如a和b,比较结果只有2种排序情况ab或ba。从比较结果来看(逆向思维),比较结果将空间分为2份,如果每次比较都能完美地二分,即二分后两边的概率是相等(即二分后左右两边处于一个平衡状态),那么对于n个数,每个数找到自己的位置,最终需要的步骤是log(n!)=O(nlogn).
快速排序就是采用这样的思想,每次都二分整个要排序的数,然后递归每个二分后的结果,所以快速排序的基本操作是划分算法。基本步骤如下
public void quickSort(int[] a ,int left, int right)
{
//选择枢纽
//划分算法
//递归左边
//递归右边
}
1)选择枢纽
快速排序的关键是如何找到这样一个枢纽,尽可能使的划分后2边的概率相等即处于一个平衡的状态,也就是说每次划分后尽可能使左边的数的个数和右边相等?对于排序中的某个数a来说,它要确定的是它最终在整个数中的位置,如果每次划分尽可能保持平衡,那么每次划分后就会将a的位置概率减少一半,那么a很快就会找到自己的位置。所以选择枢纽对快速排序算法性能影响很大。假设对每次划分后,左边有n个数,右边有1个数(这是最坏的情况,这种划分极不平衡),那么这种划分排序的效率可想而知。
2)当子数组很小的时候,使用插入排序比快速排序要快很多。
基于上述思想,优化后的快速排序的伪代码如下
//快速排序主体框架
void quickSort(left,right){
//base case
//当子数组很小的时候,选择插入排序算法
//选择划分枢纽
//两路划分算法
//递归快速排序
}//end quickSort()
//快速排序伪代码
void quickSort(left,right){
//base case
if(left>=right)
return;
//当子数组很小的时候,选择插入排序算法
if(right-left<cutoff){
insertSort(left,right);
return;
}//end if
//选择划分枢纽
pivot=getPivot(left,right);
//两路划分算法
partitionIndex=partition(left,right,pivot);
//递归快速排序
quicksort(left,pivot-1);
quicksort(pivot+1,right);
}//end quickSort()
2 枢纽的选择
你可能会说当然选择要排序数中的那个中值。good,这当然好,但是要找到这个中值,你要一个一个比较每个数,这显然降低了算法的性能。
通常有2种选择枢纽的方法,一种是每次都选择要划分数中最右边的那个数作为枢纽,另一种是三数取中(使用左端、右端和中心位置上的三个元素的中值作为枢纽)。下面分别看一下2中方法的java实现。
2.1 选择最右边的数为枢纽
package zyang.quickSort;
/**
* @author ***
* @version 1.0
* @date 2012-11-26 下午3:17:37
* @fuction 快速排序(将最右端的元素作为枢纽)
* 将最右端的元素作为枢纽,然后将所有元素划分为两组,然后将最右端的枢纽元素与枢纽位置上的元素替换(枢纽位置即为prititioin方法返回的位置,
* (这样做的目的是使枢纽元素以后不需要再排序); 接下来,再将这两个组最右端的元素分别作为两组的枢纽继续划分,依次类推下去,直到只剩下最后一个元素。
*/
public class QuickSortByRightmost {
//API
public void quickSort(int[] a)
{
recQuickSort(a, 0, a.length-1);
}
//quick sort algorithm
private void recQuickSort(int[] a, int left,int right)
{
//base case: if size is 1,it's already sorted
if(right-left<=0)
return;
else
{
//pivot(choose the rightmost item in the array as the pivot)
int pivot=a[right];
//partition range
int partition=partition(a,left,right,pivot);
//sort left side
recQuickSort(a, left, partition-1);
//sort right side
recQuickSort(a, partition+1, right);
}
}
//partition algorithm,两路划分函数
private int partition(int[] a,int left, int right, long pivot)
{
int leftPtr=left-1;
int rightPtr=right;
while(true)
{
//from left to
//check for the end of the array(leftPtr<right),不能是leftPtr<=right这样会越界
/*选择最右边的元素作为枢纽的原因
* Why can we eliminate leftPtr<right? Because we selected the rightmost item as the pivot, so leftPtr will always stop there.
Choosing the rightmost item as the pivot is thus not an entirely arbitrary choice; it speeds up the code by removing an unnecessary test.
Picking the pivot from some other location would not provide this advantage.
*/
while(a[++leftPtr]<pivot);
//from right to left for finding smaller item
while(rightPtr>0 && a[--rightPtr]>pivot);
//base case
if(leftPtr>=rightPtr)
break;
//swap
else
swap(a,leftPtr,rightPtr);
} //end while(true)
//restore pivot(找到partition位置后,将pivot即right与partition交换)
swap(a, leftPtr, right);
return leftPtr;
}
private void swap(int[] a,int left, int right)
{
int temp=a[left];
a[left]=a[right];
a[right]=temp;
}
}
代码第66行swap(a,leftPtr,rightPtr); 为什么要将找到的划分位置leftPtr和一开始定好的枢纽rightPtr交换呢?
代码56行 while(rightPtr>0 && a[--rightPtr]>pivot); 可知,第一次划分是从rightPtr-1开始的,经过一次划分后,我们比较下划分前后的效果,如下图:
上图中划分后的位置上的数是63,而枢纽还是在最有段即数36,我们需要将枢纽插入划分位置处。因为划分的目的不要排序好每一个数,划分的目的是使划分位置左边的数小于枢纽元素,而右边的数大于枢纽元素,说白了,就是要使排序的数减小要排序数最终位置的概率,如上图中数27的位置一开始可能是整个数组中的某个位置,但是经过划分后它的位置确定在数组第一个元素和划分位置之间。这里我们只需要交换划分位置处的元素和枢纽元素即可。如下图的效果:
2.2 三数取中
package zyang.quickSort;
import javax.swing.text.html.HTMLDocument.HTMLReader.ParagraphAction;
/**
* @author yangzhong E-mail:yangzhonglive@gmail.com
* @version 1.0
* @date 2012-11-26 下午3:17:37
* @fuction 快速排序(将数组最左端,中间值,最右端3个数中的不是最大且不是最小的那个数作为枢纽)
* 1 特征
* 时间复杂度:N*logN
* 最坏:O(n^2)
* 空间复杂度:O(n*logn)
* 不稳定
* 2 最坏情况
* 最坏情况发生在划分过程产生的俩个区域分别包含n-1个元素和一个元素的时候,
即假设算法每一次递归调用过程中都出现了,这种划分不对称。那么划分的代价为O(n),
因为对一个大小为0的数组递归调用后,返回T(0)=O(1)。
估算法的运行时间可以递归的表示为:
T(n)=T(n-1)+T(0)+O(n)=T(n-1)+O(n).可以证明为T(n)=O(n^2)。
因此,如果在算法的每一层递归上,划分都是最大程度不对称的,那么算法的运行时间就是O(n^2)。
3 最快情况
最快情况下,即PARTITION可能做的最平衡的划分中,得到的每个子问题都不能大于n/2.
因为其中一个子问题的大小为|_n/2_|。另一个子问题的大小为|-n/2-|-1.
在这种情况下,快速排序的速度要快得多。为,
T(n)<=2T(n/2)+O(n).可以证得,T(n)=O(nlgn)
* It is no faster when sorting random data; it’s advantages become evident only when sorting ordered data.
*/
public class quickSortByMedian {
//API
public void quickSort(int[] a)
{
recQuickSort(a, 0, a.length-1);
}
//quick sort algorithm
private void recQuickSort(int[] a, int left,int right)
{
//base case: manual sort if size is 3
int size=right-left+1;
if(size<=3)
manualSort(a,left,right);
else //quick sort if size is lager than 3
{
//pivot(choose the media item in leftmost, median, rightmost item in the array as the pivot)
int pivot=medianOf3(a,left,right);
//partition range
int partition=partionIt(a,left,right,pivot);
//sort left side
recQuickSort(a, left, partition-1);
//sort right side
recQuickSort(a, partition+1, right);
}
}
//choose the pivot
//选择枢纽pivot,对left,center,right3个元素升序排序,选择排序后的center作为pivot,然后与right-1元素交换(因为right元素已经比pivot大了,不需要与right元素交换)
private int medianOf3(int[] a,int left,int right)
{
int center=(left+right)/2;
//order left & center
if(a[left]>a[center])
swap(a,left,center);
//order left & right
if(a[left]>a[right])
swap(a,left,right);
//order center & right
if(a[center]>a[right])
swap(a,center,right);
//put pivot on right
swap(a,center,right-1);
return a[right-1];
}
//partition algorithm
private int partionIt(int[] a,int left, int right, long pivot)
{
// int leftPtr=left-1;
int leftPtr=left;
// int rightPtr=right;
int rightPtr=right-1;
while(true)
{
//from left to
//check for the end of the array(leftPtr<right),不能是leftPtr<=right这样会越界
while(a[++leftPtr]<pivot);
//from right to left for finding smaller item
// while(rightPtr>0 && a[--rightPtr]>pivot);
while(a[--rightPtr]>pivot);
//base case
if(leftPtr>=rightPtr)
break;
//swap
else
swap(a,leftPtr,rightPtr);
} //end while(true)
//restore pivot(找到partition位置后,将pivot即right与partition交换)
// swap(a, leftPtr, right);
swap(a, leftPtr, right-1);
return leftPtr;
}
private void swap(int[] a,int left, int right)
{
int temp=a[right];
a[left]=a[right];
a[right]=temp;
}
private void manualSort(int[] a,int left,int right)
{
int size=right-left+1;
if(size<=1) //no sort necessary if size is 1
return;
if(size==2) //size is 2
{
if(a[left]>a[right])
swap(a, left, right);
return;
}
else //size is 3
{
if(a[left]>a[right-1])
swap(a,left,right-1);
if(a[left]>a[right])
swap(a,left,right);
if(a[right-1]>a[right])
swap(a,right-1,right);
}
} //end manualSort()
}