为啥 Collections.sort 使用 Mergesort 而 Arrays.sort 不使用?

Posted

技术标签:

【中文标题】为啥 Collections.sort 使用 Mergesort 而 Arrays.sort 不使用?【英文标题】:Why does Collections.sort use Mergesort but Arrays.sort does not?为什么 Collections.sort 使用 Mergesort 而 Arrays.sort 不使用? 【发布时间】:2015-11-26 20:23:55 【问题描述】:

我正在使用 JDK-8 (x64)。对于Arrays.sort(原语),我在 Java 文档中找到了以下内容:

排序算法是 Vladimir Yaroslavskiy、Jon Bentley 和 Joshua Bloch 的 Dual-Pivot Quicksort。`

对于Collections.sort(对象),我找到了这个“Timsort”:

这个实现是一个稳定的、自适应的、迭代的mergesort ...这个实现将指定的列表转储到一个数组中,对数组进行排序,然后遍历列表重置数组中对应位置的每个元素。

如果Collections.sort 使用数组,为什么不直接调用Arrays.sort 或使用双轴QuickSort?为什么要使用合并排序

【问题讨论】:

这是基本数组的 javadoc - 对象数组使用 meregsort 排序。 mergesort 总是给你 nlogn,而快速排序有时可能会给出 nlogn2,通常数组大小不是那么大,但集合很容易达到数百万个条目,所以冒着 nlogn2 的风险是不值得的。 nlogn2 我的意思是 n 的平方 O(n^2) 的快速排序是最坏的情况。在实践中它更快 但是在制作 api 时不能忽略这些情况 This link 非常相关。 【参考方案1】:

API 保证了Quicksort 不提供的稳定 排序。但是,当按自然顺序对原始值进行排序时,您不会注意到差异,因为原始值没有标识。因此,Quicksort 可以用于原始数组,并且会在被认为更有效时使用¹。

对于您可能注意到的对象,当具有不同身份的对象根据其equals 实现或提供的Comparator 被视为相等时更改其顺序。因此,Quicksort 不是一个选项。因此使用了MergeSort 的变体,当前的Java 版本使用TimSort。这适用于 Arrays.sortCollections.sort,尽管使用 Java 8,List 本身可能会覆盖排序算法。


¹Quicksort 的效率优势是在原地完成时需要更少的内存。但它在最坏情况下的性能非常惊人,并且无法利用数组中预排序数据的运行,TimSort 就是这样做的。

因此,排序算法从一个版本到另一个版本都进行了重新设计,同时保留在现在被误导性命名的类 DualPivotQuicksort 中。此外,文档没有跟上,这表明,在没有必要的情况下,在规范中命名内部使用的算法通常是一个坏主意。

目前情况(包括Java 8到Java 11)如下:

一般情况下,原始数组的排序方法只会在特定情况下使用Quicksort。对于较大的数组,他们会首先尝试识别预排序数据的运行,就像TimSort 所做的那样,并在运行次数不超过某个阈值时将它们合并。否则它们将回退到Quicksort,但是对于小范围的实现将回退到Insertion sort,这不仅会影响小数组,还会影响快速排序的递归。 sort(char[],…)sort(short[],…) 添加另一个特殊情况,将 Counting sort 用于长度超过某个阈值的数组 同样,sort(byte[],…) 将使用Counting sort,但阈值要小得多,这与文档形成了最大的对比,因为sort(byte[],…) 从不使用快速排序。它只对小数组使用Insertion sort,否则使用Counting sort。

【讨论】:

嗯,有趣的是,Collections.sort Javadoc 声明:“这种排序保证是稳定的”,但是由于它委托给 List.sort,它可以被列表实现覆盖,所以稳定的排序实际上不能由 Collections.sort 保证所有列表实现。还是我错过了什么?而且 List.sort 不需要排序算法是稳定的。 @Puce:这仅仅意味着该保证的责任现在掌握在那些实现覆盖 List.sort 方法的人手中。 Collections.sort 永远无法保证每个 List 实现的正确工作,因为它不能保证,例如List 不会虚假地更改其内容。这一切都归结为Collections.sort 的保证仅适用于正确的List 实现(以及正确的Comparatorequals 实现)。 @Puce:但你是对的,Javadoc 对这两种方法中的这种约束并不同样明确,但至少最近的文档表明Collections.sort 将委托给List.sort。跨度> @Puce:有很多这样的例子,其中重要的属性不是类型的一部分,而是只在文档中提到(因此编译器不会检查)。 Java 的类型系统太弱了,无法表达任何有趣的属性。 (在这方面,它与动态类型语言没有太大区别,在文档中也定义了属性,程序员要确保它们不被违反。)实际上,它更进一步:你注意到了吗Collections.sort 甚至没有在其类型签名中提到输出已排序? 在具有更具表现力的类型系统的语言中,Collections.sort 的返回类型类似于“与输入具有相同类型和长度的集合,其属性为 1) 每个元素都存在在输入中也存在于输出中,2)对于输出中的每一对元素,左侧不大于右侧,3)对于输出中的每对相等元素,左侧的索引在输入小于正确的”或类似的东西。【参考方案2】:

我不了解文档,但在 Java 8 (HotSpot) 中 java.util.Collections#sort 的实现是这样的:

@SuppressWarnings("unchecked", "rawtypes")
public static <T> void sort(List<T> list, Comparator<? super T> c) 
    list.sort(c);

List#sort 有这个实现:

@SuppressWarnings("unchecked", "rawtypes")
default void sort(Comparator<? super E> c) 
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) 
        i.next();
        i.set((E) e);
    

所以,最后,Collections#sort 在幕后使用了Arrays#sort(对象元素)。此实现使用归并排序或 tim 排序。

【讨论】:

【参考方案3】:

根据 Javadoc,只有原始数组使用 Quicksort 进行排序。对象数组也使用 Mergesort 进行排序。

所以 Collections.sort 似乎使用与 Arrays.sort 相同的排序算法。

另一个问题是为什么原始数组与对象数组使用不同的排序算法?

【讨论】:

【参考方案4】:

正如许多答案所述。

Arrays.sort 使用 Quicksort 对原始集合进行排序,因为不需要稳定性(您不会知道或关心是否在排序中交换了两个相同的整数)

MergeSort 或更具体地说 Timsort 被 Arrays.sort 用于对对象集合进行排序。需要稳定性。 Quicksort 不提供稳定性,Timsort 提供。

Collections.sort 委托给 Arrays.sort,这就是为什么您会看到 javadoc 引用 MergeSort。

【讨论】:

【参考方案5】:

在归并排序方面,快速排序有两个主要缺点:

当涉及到非原始时,它是不稳定的。 它不保证 n log n 性能。

稳定性对于原始类型来说不是问题,因为没有区别于(值)相等性的身份概念。

对任意对象进行排序时,稳定性很重要。无论输入是什么,合并排序都能保证 n log n(时间)性能,这是一个很好的附带好处。 这就是为什么选择归并排序来提供稳定排序(Merge Sort)对对象引用进行排序的原因。

【讨论】:

“不稳定”是什么意思?

以上是关于为啥 Collections.sort 使用 Mergesort 而 Arrays.sort 不使用?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我不能在我的 ArrayList<T> 上调用 Collections.sort()?

为啥 collections.sort 在 Java 中按比较器排序时会抛出不支持的操作异常?

█■为啥要用实现接口的类实例化接口呢?

Collections.sort 使用啥设计模式?

java中排序函数sort()使用,Arrays.sort()和Collections.sort()

关于Java中Collections.sort和Arrays.sort的稳定性问题