码迷,mamicode.com
首页 > 编程语言 > 详细

快速排序中各种分区算法的学习

时间:2015-08-17 10:18:12      阅读:126      评论:0      收藏:0      [点我收藏+]

标签:

快速排序在很多教科书上被称为是起泡排序的改进,但这并不能帮我更好地理解快速排序,一直都是死记硬背,一考完试就很容易忘。

在算法设计与分析的书本上,起泡一般都归为蛮力法,而快排则是分治技术中的一种。快排的思路是:

1.选定一个“中轴”
2.进行分区
3.对分区的两个部分分别重复上述步骤

有了这么个分类,理解和记忆上都上了一个台阶。分治法的精髓就是把一个事情分成两个(或多个)小事情,再分别处理。只要能用这种思想处理的问题,都可以用分治法来试试。

快排的主函数代码可以这样写,相当典型的分治法实现:

	public void quicksort(int[] n,int i,int j){
		if(i<j){
			int s = partition(n,i,j);
			quicksort(n,i,s-1);
			quicksort(n,s+1,j);
		}
	}

难点和重点是分区的实现,分区的主要任务是,找到选取的中轴的最终位置,从而把集合分成两个区,左边的都不大于它,右边的都不小于他。我们可以有如下单元测试(groovy单元测试,中轴的选取,默认为最左边的元素,当然可以把中轴选取的策略抽象出来,但最终都可以将中轴和最左边的元素调换回到这个起点):

	static int[] data = [2,40,1,10,6,8,7,80,28]
	
	@Test
	public void testPartition(){
	        //都以最左边的元素为中轴
		assertEquals 0,Median.partition(data,0,1)
		assertEquals 2,Median.partition(data,1,2)
		assertEquals 1,Median.partition(data,0,8)
	}

有一种很简单直观的方法,有帖子俗称“挖坑填数”【1】,的确很形象。在严蔚敏老师的《数据结构》中也是这种实现方式(严老师的书中,很多地方都会把数组的第0个元素留出来用做“哨兵”,一个非常值得学习的技巧。严老师的快排没有用哨兵,但是第0个位置还是被留出来了--用来保存节点数据,这个地方需要注意一下)。java实现大致如下:

	public static int partition(int[] array,int low,int high){
		int pivot = array[low];
		while(low < high){
			while(array[high] >= pivot && low < high) high--;
			array[low] = array[high];
			while(array[low] <= pivot && low < high) low++;
			array[high] = array[low];
		}
		array[low] = pivot;
		return low;
	}

不过这种“挖坑填数”的方法,在每层循环中都必须检测 low < high 是否成立,这样才能保证分区完成时 low == high,并且这个位置就是中轴的最终位置。

而算法设计与分析基础的书上给的分区方法如下,它从两头扫描,直到i,j相交,j是中轴的最终位置:

	public int partition(int[] n,int l,int r){
		int mid = n[l];	//选择第一个元素为中轴
		int i=l,j=r+1;
		do{
			while(n[++i]<mid && i<r);	//防止i越界
			while(n[--j]>mid );		//由于n[l]==mid,所以j最坏情况是j==l,不会越界
			swap(n,i,j);
		}while(i<j);
		swap(n,i,j);	//撤销最后一次交换
		swap(n,l,j);
		return j;
	}
	
	public void swap(int[] n,int i,int j){
		int temp = n[i];
		n[i] = n[j];
		n[j] = temp;
	}

这种分区方法,只要保证下标(这里是 i 和 j )不越界就可以了。有个巧妙的技巧是用“三平均分法”来选取中轴,类似“哨兵”的思想来保证下标不越界,从而减少比较开销。(当然也可以用“哨兵”的思想,设n末尾加个值为mid的元素,保证i不越界)

	public int partition(int[] n,int l,int r){
		int midpos = getMidPosition(n, l, r);
		swap(n,l,midpos);	//交换中间值和第一个元素的位置
		int mid = n[l];		//选择第一个元素为中轴
		int i=l,j=r+1;
		do{
			while(n[++i]<mid );	
			while(n[--j]>mid );	//由于n[l] == mid,所以j是不会越界的,最坏情况是j==l
			swap(n,i,j);
		}while(i<j);
		swap(n,i,j);	//撤销最后一次交换
		swap(n,l,j);
		return j;
	}
	
	public void swap(int[] n,int i,int j){
		int temp = n[i];
		n[i] = n[j];
		n[j] = temp;
	}
	
	//此步骤可以优化,但是要保证当l-r<2时,取到较小的元素为中间值,这样才能防止i越界
	public int getMidPosition(int[] n,int l,int r){
		int a=l,b=(l+r)/2+(l+r)%2,c=r;
		if(n[a]>=n[b] && n[b]>=n[c]) return b;
		if(n[c]>=n[b] && n[b]>=n[a]) return b;
		if(n[b]>=n[a] && n[a]>=n[c]) return a;
		if(n[c]>=n[a] && n[a]>=n[b]) return a;
		return c;
	}

分区的思想,还可以用在解决计算中值和选择问题上。

	//中值问题
	public static int getMedian(int[] array){
		return getKth(array,array.length/2);
	}
	//选择问题
	public static int getKth(int[] array,int k){
		int low =0,high = array.length -1;
		int s = 0;
		do{
			s = partition(array,low,high);
			if(s>k) high = s-1;
			else low = s+1;
		}while(s != k);
		return array[s];
	}

参考资料:

【1】白话经典算法系列 http://blog.csdn.net/morewindows/article/details/6684558 








快速排序中各种分区算法的学习

标签:

原文地址:http://my.oschina.net/amhuman/blog/493331

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!