为啥 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.sort
和 Collections.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
实现(以及正确的Comparator
或equals
实现)。
@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 中按比较器排序时会抛出不支持的操作异常?