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

总结一下快速排序,如有错误或者不足,欢迎交流讨论。

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. http://blog.csdn.net/ns_code/article/details/20306991
5. http://blog.csdn.net/u011116672/article/details/50134835

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值