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个元素,同时,在最后一趟中,只需要用三数中值分割法进行排序就可以结束了。但是个人觉得这样实现有点傻傻的,其实更好的解决方式是在最后放入一个插入排序,因为在数据量很小的情况下,插入排序的效率十分高,两者排序算法结合用肯定比上面直接用三数中值分割法会好得多。
希望对你有所帮助
- java实现排序(6)-快速排序
- java实现快速排序
- 快速排序Java实现
- 快速排序java实现
- 快速排序JAVA实现
- Java实现快速排序
- 快速排序--Java实现
- 快速排序java实现
- java实现快速排序
- java实现快速排序
- Java实现快速排序
- Java实现快速排序
- 快速排序java实现
- 快速排序Java实现
- Java实现快速排序
- java快速排序实现
- JAVA实现快速排序
- java实现快速排序
- 进程间通信——共享内存
- hiho #1041 : 国庆出游
- kafka学习资料整理
- 深度学习框架Keras使用心得
- 作为前端你不得不知-浏览器的工作原理:网络浏览器幕后揭秘
- java实现排序(6)-快速排序
- C语言预处理
- C++中string erase函数的使用
- Thread类的interrupt(),interrupted(),isInterrupted()
- eclipse 总是崩掉,soup_session_feature_detach出错
- 解决idea中文乱码问题
- 二叉树的镜像
- filter(过滤器)与拦截器(AOP)区别
- 2.1.2 骑自行车的最短时间