您的位置:首页 > 编程语言 > Java开发

快速排序(附Java实现和分析)

2016-04-07 12:21 666 查看
总结一下快速排序,如有错误或者不足,欢迎交流讨论。

1、快速排序的思路

快速排序和归并排序的思路很相似,都是采取的分治思想。快速排序通过选择一个元素,该元素称为枢轴元素或切分元素,然后将它放到一个合适的位置上,使得它前面的元素不大于它,它后面的元素不小于它,然后将枢轴元素为分界点,两边的数组也采取类似的方法,即选取枢轴元素,使得前面的元素不大于它,后面的不小于它,重复进行下去,直到数组里面只有一个元素(递归退出条件)。

2、partition函数

从上述的描述来看,快速排序是需要递归的,递归地选取枢轴元素进行切分。所以,快速排序的实现重点是切分(partition)函数,即如何实现对于某一切分元素,使得它前面的元素不大于它,后面的不小于它。

3.1 partition函数实现之一

《Algorithm Fourth Edition》上的思路:对于某一枢轴元素,从第一元素开始往后扫描,找到第一个大于它的元素,然后从最后一个元素往前扫描,找到第一个小于它的元素,交换两个元素。要注意扫描不能出现数组访问越界,且扫描开始位置不能相交。



package c2Sorting;
/**
* 快速排序的第一种partition实现
* @author 小锅巴
* @date 2016年4月2日上午10:03:53
* http://blog.csdn.net/xiaoguobaf */
public class QuickSort_1 {
public static void sort(int[] a){//驱动程序
sort(a, 0, a.length-1);
}
private static void sort(int[] a, int lo, int hi){
if(lo >= hi)//递归退出判断条件
return;
int p = partition(a, lo, hi);//对于某一元素,其本身不必参与递归了,因为其所在的位置已经满足前面的不大于,后面的不小于
sort(a, lo, p-1);
sort(a, p+1, hi);
}
private static int partition(int[] a, int lo, int hi){
int left = lo;//左pointer,供扫描用
int right = hi+1;//右pointer,供扫描用,加1是为了方便扫描的推进,
int pivot = a[lo];

while(true){
while(a[++left] <= pivot)//从lo开始,找到大于pivot的元素,在访问数组时使用前++更安全,后++可能会发生越界
if(left == hi)//防止越界
break;
while(a[--right] >= pivot )//从hi开始,找到小于pivot的元素
if(right == lo)//防止越界
break;
if(left >= right)//左右扫描相交,迭代结束判断条件,相等的时候说明就是和pivot相等的元素
break;
swap(a, left, right);//交换pivot前面大于pivot的元素和pivot后面小于pivot的元素,
//从这里可以看出快速排序不稳定,因为两者之间存在和此时的left或者right相等的元素时,原有的顺序就被破坏了
}
swap(a, lo, right);//将枢轴元素放到合适的位置
//pivot未交换到合适的位置之前,其他位置的元素都满足扫描条件了(两个while里面为真),然后再进行一次扫描,扫描条件均为假了,right<=left,right所在位置的元素是不大于pivot的
return right;//返回切分元素的位置
}
private static void swap(int[] a, int i, int j){
//对于待排序数组中无重复元素时,可以使用异或操作来实现,但是如果有重复的,那么就不可以,重复的元素会被置为0
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//单元测试
public static void main(String[] args) {
int[] a = {3,4,1,9,3,2,1,6,8,4,7,5};
sort(a);
for (int i = 0; i < a.length; i++)
System.out.print(" "+a[i]+" ");
}
}
/**
* 输出: 1  1  2  3  3  4  4  5  6  7  8  9
*
*/


3.2 partition函数实现之二

选择第一个元素为枢轴元素,使用index为当前扫描元素的pointer,storIndex表示枢轴元素后面最后一个小于枢轴元素的pointer,从枢轴元素后面的第一个元素开始从左往右扫描,若当前扫描的元素比枢轴元素小,则交换index与++storIndex的元素(即第一个不小于枢轴元素的元素),进行一趟扫描后,将枢轴元素与storIndex的元素相交换,以将枢轴元素放到合适的位置。

点击这里选择QUICK即可查看动态执行情况。

package c2Sorting;
/**
* 快速排序的第二种partition实现
* @author 小锅巴
* @date 2016年4月2日下午4:24:47
* http://blog.csdn.net/xiaoguobaf */
public class QuickSort_2 {
public static void sort(int[] a){
sort(a, 0, a.length-1);
}
private static void sort(int[] a, int lo, int hi){
if(lo >= hi)
return;
int p = partition(a, lo, hi);
sort(a, lo, p-1);
sort(a, p+1, hi);
}
private static int partition(int[] a, int lo, int hi){
//我在实现这个partition函数时,感觉访问数组时使用后++很不安全,搞不好会出现栈溢出、空指针异常
int index=lo;//当前扫描元素的pointer
int storIndex = lo;//最后一个小于枢轴元素的pointer
while(++index <= hi)
if(a[index] < a[lo])
swap(a,index,++storIndex);//交换当前元素与第一个不小于枢轴元素的元素
swap(a,lo,storIndex);//将枢轴元素放到合适的位置
return storIndex;//返回枢轴元素的位置,即索引
}
private static void swap(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//单元测试
public static void main(String[] args) {
int[] a = {3,4,1,9,3,2,1,6,8,4,7,5};
sort(a);
for (int i = 0; i < a.length; i++)
System.out.print(" "+a[i]+" ");
}
}
/**
输出:
1  1  2  3  3  4  4  5  6  7  8  9
*/


3.3 partition函数实现之三

先将枢轴元素临时保存起来,从右往左扫描,找到第一个小于枢轴元素的元素,将其放到枢轴元素的位置,然后从左往右扫描,找到第一个大于枢轴元素的元素,将它放到之前第一个小于枢轴元素的位置。

package c2Sorting;
/**
* 快速排序的第三种partition实现
* @author 小锅巴
* @date 2016年4月2日上午11:39:05
* http://blog.csdn.net/xiaoguobaf */
public class QuickSort_3 {
public static void sort(int[] a){
sort(a, 0, a.length-1);
}
private static void sort(int[] a, int lo, int hi){
if(lo >= hi)
return;
int p = partition(a, lo, hi);
sort(a, lo, p-1);
sort(a, p+1, hi);
}
private static int partition(int[] a, int lo, int hi){
int pivot = a[lo];
while(lo < hi){
while(lo < hi && a[hi] >= pivot)
hi--;
a[lo] = a[hi];//将从右到左第一小于pivot的元素放到切分元素的位置
while(lo < hi && a[lo] <= pivot)
lo++;
a[hi] = a[lo];//将上一步的位置填充为从左到右第一个大于pivot的元素,此时的lo位置的元素已经不是pivot了
}
a[lo] = pivot;//退出时,lo=hi了,此位置即是切分元素应该插入的正确位置
return lo;
}

//单元测试
public static void main(String[] args) {
int[] a = {3,4,1,9,3,2,1,6,8,4,7,5};
sort(a);
for (int i = 0; i < a.length; i++)
System.out.print(" "+a[i]+" ");
}
}
/**
输出:
1  1  2  3  3  4  4  5  6  7  8  9
*/


对比三种实现,第二种最不好,相比第一种实现方式,同样一趟排序,平均情况下其交换次数第二种要比第一种至少多一倍;对比第一种和第三种实现方式,第三种访问数组的次数要少些,因为第一种采取的是交换,第一种很好理解,实现起来也容易,第三种代码紧凑些,理解稍微难那么点。

4、快速排序的改进

4.1 改进枢轴元素的选取

最好情况下,枢轴元素应该是所有元素的平均值,即中值,这样就更接近归并排序的切分情况。但是前面的三种partition实现都是选取的第一个元素为枢轴元素,并不能有这个保证,采取三数中值法(三取样切分),比较lo,mid,hi的大小,选取中间的一个作为枢轴元素。

//三取样切分
private static int threeMedium(int[] a, int lo, int mid, int hi){
return ( a[lo]<a[mid] ) ?
( a[mid]<a[hi] ? mid : (a[lo]<a[hi]) ? hi : lo ):
( a[lo]<a[hi] ? lo : a[mid]<a[hi] ? hi : mid );
}


其实还可以5取样切分,那样会更接近中数,但是过于繁琐。

4.2 切换到插入排序

对于小规模数组,插入排序够用了,用快速排序多次切分访问数组的次数将比插入排序多些,还不如用插入排序,故数组规模较小时,切换到插入排序。

最后附上改进后的快速排序Java实现

package c2Sorting;
/**
* 改进的快速排序
* @author 小锅巴
* @date 2016年4月6日下午10:38:53
* http://blog.csdn.net/xiaoguobaf */
public class QuickSort {
private static final int CUTOFF = 10;//若数组大小不超过CUTOFF,则切换到插入排序

public static void sort(int[] a){
sort(a, 0, a.length-1);
}
private static void sort(int[] a, int lo, int hi){
if(lo+CUTOFF >= hi){//切换到插入排序,调用插入排序后直接返回
insertionSort(a);
return;
}
if(lo >= hi)
return;

//将三取样的中数和lo交换
int m = threeMedium(a, lo, lo+(hi-lo)/2, hi);
swap(a, m, lo);

int p = partiton(a, lo, hi);
sort(a, lo, p-1);
sort(a, p+1, hi);
}
private static int partiton(int[] a, int lo, int hi){
int pivot = a[lo];
while(lo < hi){
while(lo < hi && a[hi] >= pivot)
hi--;
a[lo] = a[hi];
while(lo < hi && a[lo] <= pivot)
lo++;
a[hi] = a[lo];
}
a[lo] = pivot;
return lo;
}
//三取样切分 private static int threeMedium(int[] a, int lo, int mid, int hi){ return ( a[lo]<a[mid] ) ? ( a[mid]<a[hi] ? mid : (a[lo]<a[hi]) ? hi : lo ): ( a[lo]<a[hi] ? lo : a[mid]<a[hi] ? hi : mid ); }
private static void swap(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}

//切换到插入排序
private static void insertionSort(int[] a){
for(int i = 1 ; i < a.length;i++)
for(int j = i ; j > 0 && a[j] < a[j-1];j--)
swap(a, j, j-1);
}
//单元测试
public static void main(String[] args) {
int[] a = {3,4,1,9,3,2,1,6,8,4,7,5};
sort(a);
for (int i = 0; i < a.length; i++)
System.out.print(" "+a[i]+" ");
}
}
/**
输出: 1 1 2 3 3 4 4 5 6 7 8 9
*/


5、快速排序的分析

5.1 时间复杂度

快速排序和归并排序一样,都使用了递归,故可以借助递归公式来分析。对于一个未采取插入排序转换和三取样切分的快速排序,其运行时间等于两个子数列排序的时间加上在切分上花费的时间,因为扫描,显然切分花费的时间与数组规模正相关,故得到地推公式:

T(N)= T(i)+T(N-i-1)+cN

N为数组规模,i为切分后其中较小一部分的元素个数,c为某一常数

(1)最坏情况

枢轴元素始终是最小元素,此时i始终为0,T(0)=T(1)=1,与问题规模无关,在一个地推公式中可以忽略掉,故得到:T(N)=T(N-1)+cN,反复使用该公式,直到N为2,然后累加。



(2)最好情况

最好情况下,枢轴元素是中数,为简化分析,假设两个子数组大小均为原数组的一半,分析和归并排序类似。



(3)平均情况





5.2 空间复杂度

在最好情况和平均情况下,sort递归的次数是log2N次,partition返回的枢轴元素的位置的局部变量所占用得空间就是log2N次,partition函数里面的局部变量也是与log2N成正比,即空间复杂度是O(log2N);在最坏情况下,sort递归次数是N^2,此时的空间复杂度将是O(N^2),但是这样的概率很小,经过三取样后就减小了,如果排序前打乱数组,那么这种情况出现的概率可以忽略不计,证明请参考《Algorithms Fourth Edition》。

所以快速排序的空间复杂度为O(log2N)

5.3 稳定性

不稳定有两个地方,第一个地方已经在第一种实现里面提到了,第二地方在partition函数返回前将枢轴元素放到正确位置,若待放位置前有和枢轴元素值相等的元素,则破坏了稳定性。

关于具体的比较次数和交换次数以及访问数组的总次数,可参考《Algorithms Fourth Edition》,就时间复杂度和空间复杂度的分析,可不必这样做。

参考资料:

1. 《Algorithms Fourth Edition》

2. 《数据结构与算法分析:C语言描述》

3. VisuAlgo

4. /article/1337332.html

5. /article/5357705.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: