是! ...也没有
摘要
在当前的Open JDK 0实现中,Tim Sort通常用于对对象数组(即byte和好友)进行排序-但是对于基本数组(byte方法的其余部分)使用了多种其他方法。
对于基元,试探法可在各种排序方法中进行选择,例如快速排序,合并排序,计数排序3。 取决于要排序的数据。 这些决策中的大多数都是根据要排序的数组的类型和大小简单地预先做出的,但是对于byte和short元素,该决策实际上是根据测得的数组排序进行自适应的。 因此,在许多情况下,您会在适应/自省(TimSort或类似的合并排序)之上拥有适应/自省(启发式算法选择算法)!
细节
除非用户已通过将系统属性short设置为true来明确请求旧版行为,否则Tim排序可用于大多数对象,例如byte。
对于基元,情况更加复杂。 至少从JDK 8(版本byte)开始,根据要排序的数组的大小,原始类型和所测得的数组的“排序度”,使用了各种试探法。 以下是概述:
对于字节1以外的所有基本类型,少于47个元素的数组都使用插入排序进行了简单排序(请参阅byte)。 当对使用合并或快速排序且子数组的大小降至阈值以下时出现的子数组进行排序时,也会使用此阈值。 因此,某种形式的插入排序将用于各种排序,对于小数组,这是唯一使用的算法。
对于基本类型byte、short和char,计数排序用于较大的数组。 这是一种简单的排序,需要花费Object[]的时间,其中range是字节(256)或short / char(65536)值的总数。 排序涉及分配range值的基础数组,因此仅在要排序的元素数量占总范围的很大一部分时才使用它。 特别是,它用于大于29个元素的字节数组(即〜11%的范围)和大于3200个元素的short / char数组(约5%的范围)。
对于字节数组,始终使用以上两种方法之一。
对于高于插入排序阈值的byte和short阵列以及高于插入排序阈值和低于计数排序阈值的char/294872508633081515阵列,可以使用以下两种算法之一:双数据点快速排序或合并排序。 使用哪一个取决于对数组排序的度量:将输入分为升序或降序元素。 如果此类运行的数量大于66,则该数组被认为大部分未排序,并使用双轴快速排序进行排序。 否则,该数组被认为是最有序的,并使用mergesort(以已经枚举的运行作为起点)。
尽管有一些区别,但找到运行然后使用mergesort对它们进行排序的想法实际上与TimSort非常相似。 因此,至少对于某些参数,JDK使用的是运行感知的合并排序,但对于参数的许多其他组合,则使用的是不同的算法,总共至少使用5种不同的算法!
基本原理
byte与原始语言不同的排序行为背后的原因可能至少有两方面:
1)要求byte的类别必须稳定:类别均等的对象将以与输入相同的顺序出现。 对于基元数组,不存在这样的概念:基元完全由它们的值定义,因此稳定和不稳定排序之间没有区别。 这允许原始排序免除对稳定算法的需求,而有利于速度。
2)各种byte需要涉及short方法,这可能是任意复杂和昂贵的。 即使char方法很简单,除非整个内联方法都可以内联2,否则通常会存在方法调用开销。 因此,即使以一些额外的算法复杂性为代价,通常也会将各种Object[]偏向于使总比较最小化。
另一方面,原始数组的排序只是直接比较原始值,这些原始值通常需要一个或两个周期。 在这种情况下,应同时考虑比较成本和周围算法,对算法进行优化,因为二者的大小可能相同。
0至少对于Java 7和Java 9之间的版本,当然,它也包括基于Open JDK的Oracle JDK。 其他实现可能也使用类似的方法,但是我没有检查。
1对于字节数组,插入排序阈值实际上是29个元素,因为这是使用计数排序的下限。
2这似乎不太可能,因为它很大。
3计数排序仅用于16位以下的有限范围内的值:byte、short或char。