【问题标题】:QuickSort vs MergeSort on arrays of primitives in JavaJava中基元数组的QuickSort vs MergeSort
【发布时间】:2017-08-02 23:17:42
【问题描述】:
我知道 Java 的 Arrays.sort 方法使用 MergeSort 对对象数组(或对象集合)进行排序,因为它是稳定的,而 Java 使用 QuickSort 对基元数组进行排序,因为我们不需要稳定性,因为两个相等的整数是无法区分的,即他们的身份无关紧要。
我的问题是,对于原语,为什么 Java 不使用 MergeSort 保证的 O(n log n) 时间,而是使用 QuickSort 的平均 O(n log n) 时间?在其中一个相关答案here的最后一段中,解释为:
对于引用类型,被引用的对象通常比引用数组占用更多的内存,这通常无关紧要。但是对于原始类型,完全克隆数组会使内存使用量翻倍。
这是什么意思?克隆参考仍然至少与克隆原语一样昂贵。在基元数组上使用 QuickSort(平均 O(n log n))而不是 MergeSort(保证 O(n log n) 时间)还有其他原因吗?
【问题讨论】:
标签:
java
arrays
sorting
quicksort
mergesort
【解决方案1】:
并非所有 O(n log n) 算法都具有相同的常数因子。在 99.9% 的情况下,快速排序需要 n log n 时间,运行速度比合并排序快得多。我不知道确切的乘数——它会因系统而异——但是,比如说,快速排序的平均运行速度是合并排序的两倍,并且仍然具有理论上的最坏情况 n^2 性能。
此外,快速排序首先不需要克隆数组,而归并排序不可避免。但是如果你想要一个稳定的排序,你就没有引用类型的选择,所以你必须接受副本,但你不需要接受原语的成本。
【解决方案2】:
Arrays#sort(primitive array) 不使用传统的快速排序;它使用 Dual-Pivot Quicksort,它比快速排序更快,而后者又比归并排序更快,部分原因是它不必是稳定的。
来自 javadoc:
实施说明:排序算法是 Vladimir Yaroslavskiy、Jon Bentley 和 Joshua Bloch 的 Dual-Pivot Quicksort。该算法在许多数据集上提供 O(n log(n)) 性能,导致其他快速排序降低到二次性能,并且通常比传统(单轴)快速排序实现更快。
【解决方案3】:
克隆引用仍然至少与克隆原语一样昂贵。
Java 的大多数(或全部?)实现都将对象数组实现为指向对象的指针(引用)数组。因此,如果对象的大小大于指针(引用),则克隆指针(引用)数组将比克隆对象本身消耗更少的空间。
我不知道为什么要使用“克隆”这个词。合并排序分配第二个临时数组,但该数组不是原始数组的“克隆”。相反,适当的合并排序会根据自下而上的迭代或自上而下的递归级别,交替从原始到临时或从临时到原始的合并方向。
双轴快速排序
根据我在网络搜索中可以找到的内容,Java 的双轴快速排序会跟踪“递归”,如果递归深度过大,则切换到堆排序,以保持 O(n log(n)) 时间复杂度,但是以更高的成本因素。
快速排序与归并排序
除了稳定性之外,归并排序可以更快地对指向对象的指针(引用)数组进行排序。与快速排序相比,合并排序(指针的)移动次数更多,但(通过解引用指针访问的对象)的比较次数更少。
在具有 16 个寄存器(大部分用作指针)的系统上,例如 64 位模式下的 X86,4 路合并排序与常规快速排序差不多快,但我不记得看到 4 - 通用库中的方式合并排序,至少对于 PC 而言不是。
【解决方案4】:
- QuickSort 处理随机数据的速度比 MergeSort 快大约 40%,因为数据移动较少
- QuickSort 需要 O(1) 额外空间,而 MergeSort 需要 O(n)
附: Java 标准库中既没有使用经典的 QuickSort 也没有使用 MergeSort。