排序算法——快速排序

快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序说起来稍微有些复杂,在参考了很多资料后,总结一下:

首先,在提快速排序前,需要有一个概念理解清楚,对于一个已经有序的数列,从中间挑选任意一个数,那么数的左边肯定比此数小,右边肯定比此数大(升序),乍一看是句废话,那么接着可以这么理解,该数的左右两边只要比该数小或大,打乱这一个方向的顺序,那么该数所处位置依然是正确的位置。比如:5,、1、3、6、9、8、10,以6为准,不论左右两个方向的顺序如何变换,6的位置一定是确定的,即左数第四个。

(以下所说指针,实际就是该数所处在的数组位置,即数组的标号)

在理解了这个之后(当然它还不难理解),我们可以写出一段算法,这段算法是做什么用的呢?是用来做“划分”(partition)只有理解了这段算法,快速排序才能写出来。接下来,我们来分析一下“划分”算法的步骤:

(1)选择一个数组中的数,(选法很多种)此处我以数组最右端的数为例,我们称这个数为枢纽pivot;

(2)找出一个小于枢纽的数,和一个大于枢纽的数,将它们分别置于枢纽的左右方向上;

这里如何找,不妨这样设想,对于一个升序数组来说,左方应该为小于枢纽的数,那么左方如果有大于该枢纽的数时,我们需要把这个数放到右方去;同样的,右方应为大于枢纽的数,那么右方如果有小于该枢纽的数,我们则需要把它放到枢纽的左方去;好了,都说到这里了,你一定能想明白,我们需要做的是把左方大于枢纽的数和右方小于枢纽的数交换一下位置即可。放到代码中,我们只需要从数组两头同时去找这两个数,然后交换他们就可以了。

(3)重复步骤2,直到左右方向的两个指针(或引用)汇合时,就确认了该枢纽的最终位置。

为什么这么说?因为当左右双方向的指针汇合时,说明已经对这个数组完成了遍历,已经交换完了所有数,同时两个指针汇合也意味着,汇合的位置左边没有比枢纽大的数,右边则反之,所以也就意味着这个位置就是枢纽的最终位置。

理解了划分算法后,写出划分的代码,如下:

package sortAlgorithm;

public class QuickSort {
	public static int[] arr = {10,25,5,1,30,29,36,9,11,10};
	public static int partitionIt(int left,int right,int pivot){
		int leftPtr = left;
		int rightPtr = right;
		while(true){
			
			while(leftPtr < rightPtr && arr[leftPtr] < pivot){//从左边寻找一个比枢纽大的数
				leftPtr++;//如果没有找到,则满足循环条件,左方指针+1,继续寻找
			}
			while(leftPtr < rightPtr && arr[rightPtr] > pivot){//从右边寻找一个比枢纽小的数
				rightPtr--;//如果没有找到,则满足循环条件,右方指针-1,继续寻找
			}
			if(leftPtr >= rightPtr)//程序走到这里说明:1、我们可能从左右两边找到了这2个数;2、可能全都找完了,两个指针汇合了;
				break;//如果汇合,则退出循环,返回leftPtr或rightPtr都可以,因为两个指针一定指向了同一个位置,即两个指针值相等;
			else{
				swap(leftPtr,rightPtr);//如果找到了这两个数,那么交换他们的位置
			}
		}
		return leftPtr;
	}
	
	public static void swap(int left,int right){
		int temp = arr[left];
		arr[left] = arr[right];
		arr[right] = temp;
	}
	
	public static void main(String[] args){
		System.out.println("划分前的数组:");
		for(int i=0;i<arr.length;i++){
			System.out.print(arr[i]+" | ");
		}
		System.out.println("");
		System.out.println("枢纽的位置为:"+partitionIt(0,9,20));
		System.out.println("划分后的数组:");
		for(int i=0;i<arr.length;i++){
			System.out.print(arr[i]+" | ");
		}
	}
}

下面是上边的运行结果,我们来分析一下,传递给partition的参数为left=0,right=9,pivot=20,即枢纽为20,程序返回枢纽所处位置为6,意味着arr[6]的左边均比枢纽20小,而右边均比20大,这个已经是显然易见的了,所以arr[6]这个位置即为20应该放置的最终位置。当然这段程序中,没有在这里进行赋值,但是上述提到的“划分”算法我们已经实现了。

这里我再简单描述一下,算法每步运算流程:

1、左指针为0,arr[0]=10小于枢纽20,左指针+1,继续。。arr[1]=25大于20,不满足循环条件while(leftPtr < rightPtr && arr[leftPtr] < pivot),退出循环,此时leftPtr=1;

2、右指针为9,arr[9]=10,小于枢纽20,不满足循环条件while(leftPtr < rightPtr && arr[rightPtr] > pivot),直接退出,此时rightPtr=9。

3、判断leftPtr >= rightPtr,很明显不成立,走else分支,交换arr[1]和arr[9],数组变为10 | 10 | 5 | 1 | 30 | 29 | 36 | 9 | 11 | 25 |

4、重复上述操作,直到leftPtr >= rightPtr。


看到这里,如果对于上述流程还有疑问,需要彻底琢磨透才能继续向下,否则会晕~

接上述,我们得到了一个有确定枢纽位置的数组,并且左边都小于枢纽,右边都大于枢纽了,下一步,也许你也想到了,因为“划分”后,我们已经确定了一个位置,即枢纽的位置,那么接下来我们只需要对枢纽的左侧和右侧再分别进行“划分”,这样我们每划分一次就可以得到一个确定的位置,这样不就完成了排序吗?但是划分到什么时候停止呢?实际上,当我们划分到只有一个数的时候,就可以停止了,因为一个数其实已经说明它已经放在了正确的位置上,还不明白?剩余一个数,左边没有比他小的,右边没有比他大的了,不就是有序的了?!

如何对左侧和右侧再进行划分?也许你已经想到了——递归


package sortAlgorithm;

public class QuickSort {
	public static int[] arr = {10,25,5,1,30,29,36,9,11,10};
	public static int partitionIt(int left,int right,int pivot){
		int leftPtr = left;
		//int rightPtr = right;(划分算法中为此条语句)
		//此处改为right - 1;
		int rightPtr = right - 1;
		
		while(true){
			
			while(leftPtr < rightPtr && arr[leftPtr] < pivot){//从左边寻找一个比枢纽大的数
				leftPtr++;//如果没有找到,则满足循环条件,左方指针+1,继续寻找
			}
			while(leftPtr < rightPtr && arr[rightPtr] > pivot){//从右边寻找一个比枢纽小的数
				rightPtr--;//如果没有找到,则满足循环条件,右方指针-1,继续寻找
			}
			if(leftPtr >= rightPtr)//程序走到这里说明:1、我们可能从左右两边找到了这2个数;2、可能全都找完了,两个指针汇合了;
				break;//如果汇合,则退出循环,返回leftPtr或rightPtr都可以,因为两个指针一定指向了同一个位置,即两个指针值相等;
			else{
				swap(leftPtr,rightPtr);//如果找到了这两个数,那么交换他们的位置
			}
			
		}
		swap(leftPtr,right);//改为swap(rightPtr,right)也可行,因为此时leftPtr和rightPtr必相等。
		return leftPtr;
	}
	
	public static void quickSort(int left,int right){
		if(left >= right)
			return;
		else{
			int pivot = arr[right];//枢纽的选择为数组最右侧的数据项
			int partition = partitionIt(left,right,pivot);//得到枢纽所处的位置
			quickSort(left,partition-1);//左侧的子数组递归,继续进行划分
			quickSort(partition+1,right);//右侧的子数组递归,继续进行划分
		}
	}
	
	public static void swap(int left,int right){
		int temp = arr[left];
		arr[left] = arr[right];
		arr[right] = temp;
	}
	
	public static void main(String[] args){
		/*System.out.println("划分前的数组:");
		for(int i=0;i<arr.length;i++){
			System.out.print(arr[i]+" | ");
		}
		System.out.println("");
		System.out.println("枢纽的位置为:"+partitionIt(0,9,20));
		System.out.println("划分后的数组:");
		for(int i=0;i<arr.length;i++){
			System.out.print(arr[i]+" | ");
		}*/
		System.out.println("排序前的数组:");
		for(int i=0;i<arr.length;i++){
			System.out.print(arr[i]+" | ");
		}
		System.out.println("");
		quickSort(0,9);
		System.out.println("排序后的数组:");
		for(int i=0;i<arr.length;i++){
			System.out.print(arr[i]+" | ");
		}
	}
}

其实到递归这里,已经没有难度了,快速排序难点在于划分上。看完上述代码,也许会问partition中,int rightPtr为什么改为了right - 1,原先的rightPtr = right不是挺好的吗?其实原因是因为我选定了数组最右侧的数据项作为了枢纽,而在原先“划分”算法中,这个枢纽是我直接传递给这个方法的,为了当时易于理解~所以,最右侧的那个数,不需要参与到划分中,这个数在partition方法返回前,有一步至关重要的语句,swap(leftPtr,right),这一条在做的就是把确定枢纽的位置和最右侧的数据项进行了交换!问题又来了,为什么可以进行交换?首先leftPtr=rightPtr,此处是一个分界点,该点左侧和右侧均小于或大于枢纽,并且左右两侧此时还是未排序的,左右两侧的数据再怎么乱序只要都小于或大于枢纽就没有问题,也不会有影响,所以交换枢纽和最右侧的数是不会影响后续的流程的。

快速排序运行结果:


关于快速排序的效率:

在平均狀況下,排序 n 個項目要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(n log n) 演算法更快,因為它的內部循环(inner loop)可以在大部分的架構上很有效率地被實作出來。

好了,关于快速排序就说到这里。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值