快速排序在很多教科书上被称为是起泡排序的改进,但这并不能帮我更好地理解快速排序,一直都是死记硬背,一考完试就很容易忘。
在算法设计与分析的书本上,起泡一般都归为蛮力法,而快排则是分治技术中的一种。快排的思路是:
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