文章目录
一、双路快排
1. idea
- 打乱数组
- 分割partition(让a[0]归位,其左边是比它小的,右边是比它大的)
- 递归sort
和mergesort显然不同的是,qsort是先进行本层的操作,再递归(从顶到底);而mergesort是先递归,再从底层开始逐层进行该层的操作(从底到顶)。
以上是qsort的主要思想。在此基础上,qsort也有一些不同的实现方式,如普通快排、双路快排、三路快排。普通快排似乎用到的不是太多,因为对于大量重复元素,它的表现是最差的,为n2。本节课主要学习的是双路快排和三路快排,其中三路快排对于大量重复元素的处理能够做到最好。
接下来要讲的主要是二路快排的思路和实现,三路快排在优化中讲:
2. 实现
- 代码
- 实现细节
- in-place
没有用到额外空间。可以使用额外空间,让快排更加简单并stable,但没必要。 - not stable
交换两个元素可能跨越相同的元素。 - The (j == lo) test is redundant, but the (i == hi) test is not.
因为当 i 指向的元素比low元素小时(不包括等于),i++,有可能超出high的限制(有可能所有的元素都比low元素小);当j指向的元素比low元素大时(不包括等于),j- -,但当j指向low元素时,j元素与low元素相等,则停止,不会超出low的限制。 - shuffle打乱是有必要的,因为这样可以打乱数组本身的顺序。
数组越有序,快排越慢。
当快排的每一层,low元素归位后,正好从中间二分左右两部分是最快的,因为二分的话其树的深度最浅,深度为logN,时间为NlogN(每一层花费时间都为N)。但如果每次low元素归位后,左右两部分大小很不均衡,则很慢。考虑极端情况,数组本身是升序的,如果不打乱,每次low元素归位后,其左边没有任何元素比它小,右边是所有剩下的元素(都比它大),则树的深度最深,深度为N,时间为N2。为了避免这种情况,需要打乱, - equal keys
当存在重复项时,最好的办法不是跳过,而是停止扫描,将其进行交换操作。在二路快排中的实现就是这么做的。i ++和j - -的条件分别是a[i] < a[low] 和 a[j] > a[low] ,而不是等于。也就是说,当 i 指针遇到重复项时,指针不再改变,而是与 j 元素进行交换。j 也同理。这样做的目的是让key元素归位后,其左右两边的元素都包含重复项,即图中所示,左边为<= v,右边为>= v。这样能够尽可能平分重复项,而不是让重复项集中在key元素的一边。这样能够尽可能让左右两区域均衡,减少树的深度。
- in-place
3. 复杂度分析
- 最好情况:全部二分
非灰色的元素是当前层访问到的元素。显然比最坏情况少许多。
- 最坏情况:有序数组(可以通过打乱避免)
- 总结
average case证明略,对我来说太复杂了…
最后一条的意思是,需要谨慎对待快排的一些实现方法,因为有些实现方法对于这两种情况会变成n2复杂度。当数组有序,如果不打乱,会n2;当有大量重复项,即使打乱了,如果在排序时指针直接跳过重复项而不进行交换,也可能是n2。具体见下文。
- 该算法的其他性质:in-place, not stable。
4. 一些改进方法
- 当前需要快排的区间如果非常小(小于10个),可以直接用插入排序对当前区间排序。因为快排对于很小的子数组有太多的多余开销。
- 在low元素、high元素和当前区间的中间位置元素中,找到三个元素中排序为中间的元素,将该元素作为key元素(即与low元素交换)。这样能够更大可能地均分这一次分割后的两个区间。
二、双路快排的应用:找到第k小的元素(selection)
ps. 最坏情况为线性的selection算法还没有找到很完美的。现在就使用quik-select。
三、三路快排
为了解决大量重复项的问题。
当有大量重复项时,三种快排的表现:
- 普通快排:当遇到重复项时,将其分配在key元素的同一侧。这样最慢。因为两侧不均衡。尤其是当数组中所有项都相同时,剩下的元素都在同一侧,树的深度最深。
- 双路快排:当遇到重复项时,停止指针的扫描,将 i 指针与 j 指针交换,尽可能地均匀分配重复项在key元素两侧。但是这样在每次排序之后,后面的排序还需要对重复项进行操作。
- 三路快排:最优方法,将数组分成三个区间,小于、等于和大于。每一层快排之后,都能将重复项直接归位,下一次排序只操作小于和大于部分,不再对重复项进行操作。
参考:
快速排序精讲——需要重点处理的三种特殊情况
快速排序 详解(快速排序 双路快排 三路快排)
快速排序之重复元素过多的改进
1. idea
例:当只有三个元素时,一次快排就可以排好序,不需要递归。该例子中key元素始终为R(即只是一层快排)。
2. 实现
3. 复杂度分析
Randomized quicksort with 3-way partitioning reduces running time from linearithmic to linear in broad class of applications
输入N个元素,任何排序方法的花费下限(也就是复杂度最低)是:
当N个元素都不同,是NlogN;
当只有常数数量的不同元素,剩下的都是相同的,是N。
(证明略)
三路快排已经能够做到最优。
四、系统的排序方法
- Java的arrays.sort()
对object使用的是mergesort,因为对于对象来说,可能对于空间的节省要求不那么高(对象本身就占不少空间),且mergesort是stable的。而对于原始数据类型,则使用quicksort,因为这样能够最大限度的节省空间(不需要额外空间,除了递归用到的栈),且不用担心不stable的问题(对于原始数据来说,排序就看当前元素的大小即可,元素没有其他的属性可以比较。而对象则不同,对象有好多属性可以比较,如果不stable就会很麻烦,排完这个属性,上一个属性又乱了)。
- C, C++, Java 6等系统使用的quiksort,进行了许多微调。
即使如此, Java’s system sort is not solid。当输入数据过多,这些数据会使函数调用栈溢出,并让程序崩溃。
- 如何选择一个排序算法,要考虑很多内容