java实现排序(6)-快速排序

来源:互联网 发布:c 数据库编程 编辑:程序博客网 时间:2024/06/15 15:59

引言

快速排序,作为一个编程人员来说,肯定都是接触过的。那么,你还记得怎么去实现么,怎么优化呢?在本篇博文中,会详细介绍快速排序的过程,对于不是唯一的过程(可变或者可选),我们讨论各种优化的方法。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

快速排序

在开始之前,我们先介绍一下快速排序的基本思想:
我们要对S数组进行排序,那么
①如果S数组中的元素个数是0或者1,那么排序结束。单一元素本身就是有序的。
②在S中随机选取一个元素作为枢纽元
③将S除却枢纽元之外的集合遍历与枢纽元进行比较,把比枢纽元小的放在左边,比枢纽元大的放在右边。
④遍历结束之后,就是完成了一趟快速排序。我们递归③步骤中生成的2组数据,重复①②③步骤,直到满足①条件退出为止

下面是对上面步骤的图示:
这里写图片描述

细心的小伙伴会发现,这个随机选取的枢纽元将会决定你下次分组的复杂程度。比如说在7、8、9这个组中,如果选取了7或者9为枢纽元呢?那是不是还需要更多的一步递归?所以说,枢纽元的选取将会直接影响到你的算法的效率,在后面,我们会着重讨论关于枢纽元的选取。

快速排序,和上一篇博文介绍的归并排序,都是一种分治策略的体现,我们都是把一个大的数据拆分成一个更小的数据,直到不能拆分为止,再把所有的结果进行整合,得到最终的结果。

快速排序最容易理解的实现

如果你还是对快速排序不是很理解,那么我们用一组代码实现来进一步帮助你的理解,在该实现中,借用了list集合天然的方法。我们不去考虑这一套算法的性能和效率,因为它必然是低效的。

package com.brickworkers;import java.util.ArrayList;import java.util.List;/** *  * @author Brickworker * Date:2017年5月9日下午3:24:56  * 关于类QuickSort.java的描述:快速排序 * Copyright (c) 2017, brcikworker All Rights Reserved. */public class QuickSort {    //最容易理解的快速排序    //我们以int排序为例,方便起见,就不像以前一样实现对象的compareTo方法了    public static void sort(List<Integer> list){        //递归结束条件        if(list.size() > 1){//上面说的,当数据量为1或者0的时候结束递归            //建立三个集合,分别表示小于枢纽元,大于枢纽元和等于枢纽元            List<Integer> smallerList = new ArrayList<Integer>();            List<Integer> largerList = new ArrayList<Integer>();            List<Integer> sameList = new ArrayList<Integer>();            //选取一个随机值作为枢纽元,在我们学习过程中,我们通常把第一个数作为枢纽元            Integer pivot = list.get(0);            //遍历list,把比pivot小的放smallerList中,比pivot大的放largerList中,相等的放sameList中            for (Integer integer : list) {                if(integer < pivot){                    smallerList.add(integer);                }                else if(integer > pivot){                    largerList.add(integer);                }else{                    sameList.add(integer);                }            }            //递归实现分组后的子数据进行上面同样的操作            sort(smallerList);            sort(largerList);            //对排序好的数据进行整合            list.clear();//清楚原本的数据,用于存放有序的数据            list.addAll(smallerList);            list.addAll(sameList);            list.addAll(largerList);        }    }    public static void main(String[] args) {        Integer[] target = {4,3,6,7,1,9,5,2,3,3};        List<Integer> list = new ArrayList<>();        for (int i = 0; i < target.length; i++) {            list.add(target[i]);        }        sort(list);        //查看排序结果        for (Integer integer : list) {            System.out.print(integer + " ");        }    }}//输出结果://1 2 3 3 3 4 5 6 7 9 //

细心的小伙伴可能心里有疑惑了,为什么我要用一个List来存储一样大小的呢?这里就暴露出了快速排序算法中对相同数据的处理方式。在上述的实现中为什么不直接把相等的放入smallerList或者放入largerList呢?举个例子:假如说你选取的枢纽元是最小值,那么是不是可能发生每次递归的数据都是一样的?因为所有的数据都比你的枢纽元大,而且这个时候恰好你把相等的数据都放入了比枢纽元大的部分,那么就会造成栈溢出了。所以在这个过程中,我们需要对相等的数据单独存储起来。

如何选取枢纽元

经过前面的概念分析与最傻的实现之后,我们讨论一下与快速排序效率息息相关的枢纽元选取。其实枢纽元选取的核心问题是,我们要把原本的待排序数据合理、平均的划分为两部分,我们就像要求平衡二叉树保持平衡一样。

1、以第一个值为枢纽元
在大学期间,我们学习到快速排序,告知我们的一般都是以第一个值为枢纽元。对于这种默认的选取方式,我们对他进行剖析:
①如果待排序的数据是随机的,那么如此选择枢纽元是可以接受的,因为在概率上来说,随机的情况下,在第一次快速排序之后,会分为两个差不多相等的新的数据。
②如果待排序的数据是有序的,那么这种情况下,就不能以第一个值为枢纽元了,因为它会产生一种恶心分割,直接导致所有的元素都被划分到左边子数据或者右边子数据。
所以这种办法是不可取的,也尽量不去实现它。也有人说可以选取第一个和第二个数据做比较,比较大的作为枢纽元。这种方式只是简单的规避了划分为空的情况,这种恶心的划分还是存在的。

2、随机选择一个枢纽元
在待排序的数据中随机选取一个数据为枢纽元会显得安全很多。它的随机可以保证在分割的过程中可以合理、平均的进行划分。但是我们要考虑随机数产生的开销,每趟分割之前还需要随机出一个随机数,那么开销会变得非常巨大。所以这种方式虽然比较安全,但是性能仍旧是不可取的。

3、三数中值分割法
三数中值分割法是目前比较高效的一种选取枢纽元的方式。按照选取枢纽元的要求:要尽可能合理、平均的划分为两部分。那么最好的是选取一个中值,那么就可以精准的分割两部分了。但是我们不可能划分这个开销去寻找中值,我们做的只是:
我们从待排序的数据中选取3个位置的值,分别是第一位置、最后位置、中间位置。然后我们用这3个数据中排序中间的数据作为枢纽元。

在这里,又会有细心的小伙伴有疑惑了,为什么三数中值分割法会更优秀?它需要选取3个值,还要判断拿到3个值中不大不小的那个。这么一来难道不会影响效率吗?
当然会影响效率,但是这个效率的开销是值当的,我们在选取好枢纽元之后仍旧需要遍历3个之中剩余的两个值,因为这里已经比较了一轮,我们只需要在枢纽元选取的时候就把剩余两个进行排序了,比枢纽元小的放在最前面,比枢纽元大的放在最后面,且在遍历的时候跳过这两个值。所以说三数中值分割法并没有白白花费这个效率开销。

快速排序核心思想

或许,对我上面的描述不是很理解,我们接下来已目前最好的快熟排序遍历的方式来说明这种情况:
在目前主流的快速排序中,我们把枢纽元与最后一个数据进行位置交换,也就是说把枢纽元分离出要进行数据交换的区段,然后通常是定义了2个指针,一个从开头往后比较,一个从后往前比较。进行大小比较和位置转换,且流行的情况是这样,下面我们用图解来说明情况:

我们要对数据:3,2,5,7,1,8,9进行快速排序。
我们随机选取一个值为枢纽元:5,那么我们就把5与9进行位置交换,把枢纽元独立到数据的边缘,避免它参与数据交换,所以初始情况就是这样:
3,2,9,7,1,8,5
我们定义两个指针,这两个指针我们称为头指针和尾指针,分别指向第一个元素和倒数第二个元素(倒数第一个元素是枢纽元)。接着就需要开始移动指针:
在头指针指向的位置小于尾指针指向的位置时:
①我们将头指针向右移动,遇到比枢纽元小的元素直接移动到下一个数据,直到遇到一个数据大于枢纽元,则头指针停止运动。
②相同的,移动尾指针向前移动,遇到比枢纽大的元素直接移动到下一个数据,直到遇到一个数据小于枢纽元。
③等两个指针都停止下来的时候,就需要把两个指针所指向的数据进行交换。并继续重复①②③步骤。
④直到尾指针指向的位置小于头指针指向的位置,通俗来说就是两个指针交错了就结束遍历。

下面是对于上面数据快速排序的一趟图解:
这里写图片描述

在此基础上,我们再来说说为什么三数中值分割法并没有浪费额外的开销:用三数中值分割法获取到枢纽元,那么其实比这个枢纽元小的数据可以放在最左边,比这个枢纽元大的数据放在最右边。那么这样一来,头指针就可以从第二个开始,尾指针就可以从倒数第二个开始,这样一来,一定程度上效率是有回升的。

代码实现标准快速排序

思考:
1、快速排序是分治策略的实现,所以递归必不可少。
2、采用最好的三数中值分割法选取枢纽值,并对选取后的数据进行合理存放,在上面已经描述过了。头指针可以跳过首数字,尾指针也可以跳过倒数第二个数字。但是对于子数组的大小我们要注意最小是2了,不然不满足三数中值分割法。
3、我们不用新的数组来参与递归和存储,所以我们定义好对于数组描述的下标left与right,尽可能的数组复用。

以下是代码实现:

package com.brickworkers;/** *  * @author Brickworker * Date:2017年5月9日下午3:24:56  * 关于类QuickSort.java的描述:快速排序 * Copyright (c) 2017, brcikworker All Rights Reserved. */public class QuickSort {    //暴露给外部的接口,对一个数组进行排序    public static void quicksort(Integer [] target){        quicksoort(target, 0, target.length - 1);    }    //具体实现    //用left与right的方式,尽可能的实现数组复用    private static void quicksoort(Integer[] target, int left, int right){        if(left + 2 < right){//递归结束条件,之所以+2是因为三数中值分割法最起码需要两个数据            //寻找枢纽元            int pivot = findPivot(target, left, right, false);            //定义头指针与尾指针            int i = left + 1, j = right - 2; //因为三数中值分割法导致最前数据和最后数据不用判断            for( ; ; ){                //两个指针开始运动,直到两者都停止                while(target[i] <= pivot){i++;}//如果头指针遍历到小于枢纽元的数据直接跳过                while(target[j] >= pivot){j--;}//如果尾指针遍历到大于枢纽元的数据直接跳过                //判断两个指针是否交错                if(i < j){                    //没有交错,且指针停止,那么进行数据交换                    swap(target, i, j);                }else{                    break;//指针交错,那么结束循环                }            }            //也就是上面描述的指针交错之后,需要把枢纽元交换到头指针的位置            swap(target, i, right - 1);            //继续递归子数组            quicksoort(target, left, i - 1);            quicksoort(target, i + 1, right);        }else{            //当数据少于2个的时候,直接用三数中值分割法进行排序            findPivot(target, left, right, true);        }    }    //三数中值分割法    //这个判断用于说明是否最后的操作,最后的操作不需要把枢纽值放到最后    private static Integer findPivot(Integer[] target, int left, int right, boolean end){        int mid = (left + right) / 2;//获取中间值的位置        //比较开始数据与中间数据        if(target[left] > target[mid]){            //如果开始数据比中间数据大,那么位置进行交换            swap(target, left, mid);        }        if(target[left] > target[right]){            //如果开始的数据比最后数据大,那么交换位置            swap(target, left, right);        }        if(target[mid] > target[right]){            //如果中间的数据比最后的数据大,那么交换位置            swap(target, mid, right);        }        if(!end){            //按照前面说的,把枢纽元放到最后面            swap(target, mid, right - 1);            //返回枢纽元            return target[right - 1];        }        return null;    }    //交换数组中两个下标的数据    private static final void swap(Integer[] target, int one, int anthor){        int temp = target[one];        target[one] = target[anthor];        target[anthor] = temp;    }    public static void main(String[] args) {        Integer[] target = {4,3,6,7,1,9,5,2,3,3};        quicksort(target);        for (Integer integer : target) {            System.out.print(integer + " ");        }    }}//输出结果://1 2 3 3 3 4 5 6 7 9 //

上面的实现加入了三数中值分割法,它所造成的影响就是元素判断基准变成了最起码子数组要有2个元素,同时,在最后一趟中,只需要用三数中值分割法进行排序就可以结束了。但是个人觉得这样实现有点傻傻的,其实更好的解决方式是在最后放入一个插入排序,因为在数据量很小的情况下,插入排序的效率十分高,两者排序算法结合用肯定比上面直接用三数中值分割法会好得多。

希望对你有所帮助

12 2
原创粉丝点击