快速排序


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()
	
}





  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值