Java快速排序

来源:互联网 发布:汇编语言编程pdf 编辑:程序博客网 时间:2024/06/14 01:21

引言

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

快速排序

在开始之前,我们先介绍一下快速排序的基本思想:

我们要对S数组进行排序,那么

①如果S数组中的元素个数是0或者1,那么排序结束。单一元素本身就是有序的。

②在S中随机选取一个元素作为枢纽元

③将S除却枢纽元之外的集合遍历与枢纽元进行比较,把比枢纽元小的放在左边,比枢纽元大的放在右边。

④遍历结束之后,就是完成了一趟快速排序。我们递归③步骤中生成的2组数据,重复①②③步骤,直到满足①条件退出为止

下面是对上面步骤的图示:

Java快速排序

Java学习交流群:495273252

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

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

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

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

Java快速排序

Java快速排序

Java快速排序

细心的小伙伴可能心里有疑惑了,为什么我要用一个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

我们定义两个指针,这两个指针我们称为头指针和尾指针,分别指向第一个元素和倒数第二个元素(倒数第一个元素是枢纽元)。接着就需要开始移动指针:

在头指针指向的位置小于尾指针指向的位置时:

①我们将头指针向右移动,遇到比枢纽元小的元素直接移动到下一个数据,直到遇到一个数据大于枢纽元,则头指针停止运动。

②相同的,移动尾指针向前移动,遇到比枢纽大的元素直接移动到下一个数据,直到遇到一个数据小于枢纽元。

③等两个指针都停止下来的时候,就需要把两个指针所指向的数据进行交换。并继续重复①②③步骤。

④直到尾指针指向的位置小于头指针指向的位置,通俗来说就是两个指针交错了就结束遍历。

下面是对于上面数据快速排序的一趟图解:

Java快速排序

Java学习交流群:495273252

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

代码实现标准快速排序

思考:

1、快速排序是分治策略的实现,所以递归必不可少。

2、采用最好的三数中值分割法选取枢纽值,并对选取后的数据进行合理存放,在上面已经描述过了。头指针可以跳过首数字,尾指针也可以跳过倒数第二个数字。但是对于子数组的大小我们要注意最小是2了,不然不满足三数中值分割法。

3、我们不用新的数组来参与递归和存储,所以我们定义好对于数组描述的下标left与right,尽可能的数组复用。

以下是代码实现:

Java快速排序

Java快速排序

Java快速排序

Java快速排序

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

学习Java的同学注意了!!!

学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群495273252,我们一起学Java!

0 0