排序是一个Java开发者,在日常开发过程中随处可见的开发内容,Java中有丰富的API可以调用使用。在Java语言中,作为集合工具类的排序方法,必定要做到通用、高效、实用这几点特征。使用什么样排序算法会比较合适,能够做到在尽量降低时间、空间复杂度的情况下,又要兼顾保证稳定性,达到优秀的性能。可能从性能角度出发首先会想到的是快速排序,或者归并排序。作为jdk提供的通用排序功能,使用又如此频繁,肯定有独特之处,一起来看学习下期中的奥秘。
文中不会过多的介绍几大基本排序算法的方式、由来和思想,主要精力集中在一块探讨java中排序方法所使用的算法,以及那些是值得我们学习和借鉴的内容。文中如有理解和介绍的错误,一起学习,一起探讨,一起进步。
日常使用最为频繁的排序,莫过于如下代码案例,给定一个现有的序列进行一定规则下的排序,配合java8的stream特性,可以很方便的对一个集合进行排序操作(排序规则只是对排序对象及排序方案的限定,不在本文讨论范围内)。
List<Integer> list = Arrays.asList(10, 50, 5, 14, 16, 80);
System.out.println(list.stream().sorted().collect(Collectors.toList()));
在代码执行的过程中SortedOps.java类中 Arrays.sort(array, 0, offset, comparator); 执行了Array集合类型的sort排序算法。
@Override
public void end() {
Arrays.sort(array, 0, offset, comparator);
downstream.begin(offset);
if (!cancellationWasRequested) {
for (int i = 0; i < offset; i++)
downstream.accept(array[i]);
}
else {
for (int i = 0; i < offset && !downstream.cancellationRequested(); i++)
downstream.accept(array[i]);
}
downstream.end();
array = null;
}
如果使用Collections.sort() 方法如下打印 list1 和 list2 结果一样,且调用的都是 Arrays 集合类中的 sort 方法。
List<Integer> list1 = Arrays.asList(10, 50, 5, 14, 16, 80);
System.out.println(list1.stream().sorted().collect(Collectors.toList()));
List<Integer> list2 = Lists.newArrayList();
list2.addAll(list1);
Collections.sort(list2);
System.out.println(list2);
// 输出:
// [5, 10, 14, 16, 50, 80]
// [5, 10, 14, 16, 50, 80]
Collections类中关于sort方法定义如下:
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
通过该方法注释,查看到有三项值得关注的信息,大概意思是该方法实现了稳定且默认升序排序的功能。
1. Sorts the specified list into ascending order, according to the Comparable natural ordering of its elements.
2. This sort is guaranteed to be stable equal elements will not be reordered as a result of the sort.
3. The specified list must be modifiable, but need not be resizable.
进入sort,代码进入到List类的sort方法,发现方法将入参list先转为了数组Object[],之后利用Arrays.sort进行排序。
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);
}
}
首先在这里思考一个问题为什么要转为数组,问题答案已经在方法的英文注释中说明白了。
* The default implementation obtains an array containing all elements in
* this list, sorts the array, and iterates over this list resetting each
* element from the corresponding position in the array. (This avoids the
* n<sup>2</sup> log(n) performance that would result from attempting
* to sort a linked list in place.)
是为了避免直接对List
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
Timsort是一个自适应的、混合的、稳定的排序算法,是由Tim Peter于2002年发明的,最早应用在Python中,现在广泛应用于Python、Java、Android 等语言与平台中,作为基础的排序算法使用。其中Java语言的Collection.sort在JDK1.6使用的是普通的归并排序,归并排序虽然时间复杂度低,但是空间复杂度要求较高,所以从JDK1.7开始就更改为了TimSort算法。
Timsort 的时间复杂度是 O(n log n),与归并排序的时间复杂度相同,那它的优势是啥呢,实际上可以认为TimSort排序算法是归并排序算法的优化版,从它的三个特征就可以看出,第二个特征“混合的”,没错,它不单纯是一种算法,而是融合了归并算法和二分插入排序算法的精髓,因此能够在排序性能上表现优异。其它两个特征自适应和稳定性会在文章后面讲到。首先从算法性能统计上做个对比:

可以看出TimSort排序算法,平均和最坏时间复杂度是O(nlogn),最好时间复杂度是O(n),空间复杂度是O(n),且稳定的一种排序算法。在稳定算法中,从性能效果上对比来看和二叉排序算法一样。
那TimSort算法的核心思想是什么呢,首先原始的TimSort对于长度小于64的数据(java中是32),会直接选择二分插入排序,效率很高。其次,TimSort算法的初衷认为现实中的数据总是部分有序的。这句话很关键,怎么理解呢,比如列表[5, 2, 8, 5, 7,23, 45, 63],里面的[5, 2] 和 [8, 5] 和 [7, 23, 45,63] 各子列表中就是有序的,要么升序要么降序,这就是TimSort的基本根据。
基于此会发现待排序列表已经部分有序了,所以会在排序过程中尽量不要破坏这种顺序,就可以做到减少排序时间消耗。基本思想说完了,由此引出TimSort算法的几个概念:run和minrun。
run是指连续升序或者连续降序的最长子序列(降序和升序可以相互转换),而minrun是一个设定值,实际上是每个run的长度最小值。所以TimSort会对待排序序列进行划分,找出连续有序的子序列,如果子序列长度不满足这点要求,就将后续数据插入到前面的子序列中。
举个例子,待排序序列[5, 2, 8, 5, 7,23, 45, 63], 如果minRun = 3,那分割后的run会有以下:[2, 5, 8]、[5,7,23,45,63] 两个子序列,最终通过合并这两个run得到[2,5,5,7,8,23,45,63]
是不是有个疑问: minrun怎么选择得到的?该值是通过大量的统计计算给出的minrun长度建议是在32 ~ 64之间取值效率比较高,具体在java代码中可能会有所不同。
接着来看,假设现在有序子序列已经拆分好了,需要进入到合并过程中了,TimSort是如何合并子序列的。对于归并排序我们都知道,序列先归后并,两两组合利用一个空数组直接进行比较就合并了。但是在TimSort算法中,合并过程是实时的,每次算出一个run就可能做一次合并。这个过程利用了栈结构,且需要遵循相邻的run才可以合并,也就是只有相邻的栈元素可以进行合并。
规则如下:假设当前有三个run子序列依次入栈,现在栈顶有三个元素从上至下依次为x3、x2、x1,它们的长度只要满足以下两个条件中的任何一个就进行合并:
(1)x1 <= x2 + x3
(2)x1 <= x2
满足这个条件的三个序列,像汉诺塔一样长度由下往上依次减小。刚才提到合并run的过程是实时的,也就是每产生一个run就进行一次合并操作。举例说明下,当前假设待排序序列[2,6,8,4,2,5,7,9,10,11,4,25,64,32,78,99],其中再假设minrun=3是合理的。合并过程是这样的,注意这里的压栈和弹栈不一定需要对子序列本身进行操作,不是真的将子序列放入栈中,而只需要run标识以及长度即可,因为栈元素比较的是run长度。
(1)首先第一个run0是[2,6,8],而第二个run1是[2,4,5],此时依次将其放入栈中,发现满足第二个条件,这两个run进行合并,合并后将旧序列从栈中弹出,得到新的run0是[2,2,4,5,6,8],再次压入栈中。
(2)继续从原序列中找到新的run1是[7,9,10,11],压入栈中,此时run0和run1不满足条件不需要合并。继续从原序列中找到run2是[4,25,64],压入栈中,此时满足第一个条件,这里的run1和run2需要进行合并,合并后将旧序列从栈中弹出,新run1是[4,7,9,10,11,25,64],压入栈中。
(3)此时发现run0和run1满足第二个条件,继续合并弹出旧序列,得到新run0是[2,2,4,4,5,6,7,8,9,10,11,25,64],压入栈中。
(4)继续从原序列中找到新的run1是[32,78,99],压入栈中。此时发现没有更多元素,而条件是不满足的,依然进行一次合并,弹出旧序列,压入合并后的新子序列run0是[2,2,4,4,5,6,7,8,9,10,11,25,32,64,78,99]
(5)此时将run0拷贝到原序列就完成了排序

为什么要设置这么两个合并run的严格条件,直接压栈合并岂不更好?目的是为了避免一个较长的有序片段和一个较小的有序片段进行归并,在合并长度上做到均衡效率才高。
在合并run的过程中会用到一种所谓的gallop(飞奔)模式,能够减少参与归并的数据长度,主要过程如下:假设有待归并的子序列x和y,如果x的前n个元素都是比y首元素小的,那这n个元素实际上就不用参与归并了。原因就是这n个元素本来已经有序了,归并后还是在原来的位置。同理而言,如果y的最后几个元素都比x最后一个元素小,那y的最后这n个元素也就不必参与归并操作了,这样就可以减少归并长度,减少来回复制多余数据的开销。
探讨完TimSort的核心思想及其排序过程,现在来看下java代码是如何实现的。Java1.8中的TimSort类位置在java.util.TimSort
static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
T[] work, int workBase, int workLen) {
assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;
int nRemaining = hi - lo;
if (nRemaining < 2)
return;
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
int minRun = minRunLength(nRemaining);
do {
int runLen = countRunAndMakeAscending(a, lo, hi, c);
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen, c);
runLen = force;
}
ts.pushRun(lo, runLen);
ts.mergeCollapse();
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
assert lo == hi;
ts.mergeForceCollapse();
assert ts.stackSize == 1;
}
变量nRemaining记录的是待排序列表中剩余元素个数, MIN_MERGE就是前文中提到的java中的minrun值是32。如果nRemaining<32,用countRunAndMakeAscending(…)方法得到连续升序的最大个数,里面涉及到升序降序调整。可以看到如果待排序列表小于32长度,就进行二分插入排序binarySort(…)。
如果待排序列表长度大于32,调用TimSort对象的minRunLength(nRemaining) 计算minRun,这里就体现了动态自适应,具体来看代码中是如何做的。r为取出长度n的二进制每次右移的一个溢出位值,n每次右移1位,直到长度n小于32。n+r最终结果就是保留长度n的二进制的高5位再加上1个移除位。根据注释可以看出:
假如待排序列表长度是7680,二进制是1111000000000,按照操作后是11110十进制是30,再加上移除位0是30,所以minRun=30
private static int minRunLength(int n) {
assert n >= 0;
int r = 0; // Becomes 1 if any 1 bits are shifted off
while (n >= MIN_MERGE) {
r |= (n & 1);
n >>= 1;
}
return n + r;
}
接下来在循环中进行处理:
(1) 计算最小升序的run长度,如果小于minRun,使用二分插入排序将run的长度补充到minRun要求的长度。
(2) ts.pushRun(lo, runLen) ,通过栈记录每个run的长度,这里lo是run的第一个元素的索引用来标记操作的是哪个run,runLen是run的长度。
private void pushRun(int runBase, int runLen) {
this.runBase[stackSize] = runBase;
this.runLen[stackSize] = runLen;
stackSize++;
}
(3)ts.mergeCollapse(); 通过计算前面提到的两个run合并的限定条件,分别是:
private void mergeCollapse() {
while (stackSize > 1) {
int n = stackSize - 2;
if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {
if (runLen[n - 1] < runLen[n + 1])
n--;
mergeAt(n);
} else if (runLen[n] <= runLen[n + 1]) {
mergeAt(n);
} else {
break; // Invariant is established
}
}
}
(4) 这里的mergeAt(n) 归并排序过程,之前有提到是经过优化后所谓gallop模式的归并排序,具体表现在方法中的gallopRight和gallopLeft方法。
int k = gallopRight(a[base2], a, base1, len1, 0, c);
assert k >= 0;
base1 += k;
len1 -= k;
if (len1 == 0)
return;
len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);
assert len2 >= 0;
if (len2 == 0)
return;
假设有X序列[4,9,21,23], Y序列[5,7,12,13,14,15,17],由于X中4小于Y序列最小元素5,所以合并后4必然是第一个元素;而Y序列中尾元素17比X中的[21,23]小,所以X中的[21,23]必然是合并最后两元素。
前文案例中提到SortedOps.java类,该类中对于基本类型的排序调用 Arrays.sort(ints); 或 Arrays.sort(longs); 再或 Arrays.sort(doubles); 使用了DivalQuickSort排序算法。
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
快速排序使用的是在排序序列中选择一个数作为分区点pivot,也就是所谓的轴,然后以此轴将数据分为左右两部分,大于该值的为一个区,小于该值的为一个区,利用分治和递归思想实现。如下假设选择19为pivot值。

双轴快排,如其名字所示,就是会选取两个数作为pivot,这样就会划分出三个区间,实际在执行中会有4个区间,分别是小于等于pivot1区间;pivot1和pivot2之间区间,待处理区间; 大于等于pivot2区间。如下假设选择10和19为两个pivot值。

每次递归迭代时遍历待处理的区域数据,然后比较它应该放的位置,并进行交换操作,逐渐压缩待处理区域的数据长度,处理掉待处理区域的元素数据;执行完毕一轮数据后交换pivot数值,然后各自区间再进行递归排序即可。
static void sort(int[] a, int left, int right,
int[] work, int workBase, int workLen) {
if (right - left < QUICKSORT_THRESHOLD) {
sort(a, left, right, true);
return;
}
int[] run = new int[MAX_RUN_COUNT + 1];
int count = 0; run[0] = left;
for (int k = left; k < right; run[count] = k) {
if (a[k] < a[k + 1]) { // ascending
while (++k <= right && a[k - 1] <= a[k]);
} else if (a[k] > a[k + 1]) { // descending
while (++k <= right && a[k - 1] >= a[k]);
for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {
int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
}
} else { // equal
for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {
if (--m == 0) {
sort(a, left, right, true);
return;
}
}
}
if (++count == MAX_RUN_COUNT) {
sort(a, left, right, true);
return;
}
}
if (run[count] == right++) {
run[++count] = right;
} else if (count == 1) {
return;
}
byte odd = 0;
for (int n = 1; (n <<= 1) < count; odd ^= 1);
int[] b;
int ao, bo;
int blen = right - left;
if (work == null || workLen < blen || workBase + blen > work.length) {
work = new int[blen];
workBase = 0;
}
if (odd == 0) {
System.arraycopy(a, left, work, workBase, blen);
b = a;
bo = 0;
a = work;
ao = workBase - left;
} else {
b = work;
ao = 0;
bo = workBase - left;
}
for (int last; count > 1; count = last) {
for (int k = (last = 0) + 2; k <= count; k += 2) {
int hi = run[k], mi = run[k - 1];
for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) {
if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) {
b[i + bo] = a[p++ + ao];
} else {
b[i + bo] = a[q++ + ao];
}
}
run[++last] = hi;
}
if ((count & 1) != 0) {
for (int i = right, lo = run[count - 1]; --i >= lo;
b[i + bo] = a[i + ao]
);
run[++last] = right;
}
int[] t = a; a = b; b = t;
int o = ao; ao = bo; bo = o;
}
}
通过看代码可以看出,java中的双轴快排在片段数据长度及一定条件的情况下,还使用了其它诸如归并、插入等排序算法。
由于DivalQuickSort算法实现内容比较复杂,文中重点讲解了TimSort算法,待笔者研究透彻后进行补充。
想要真正模拟一模一样运行环境,是困难的。这里只是模拟相同的数据集,在相同机器上,其实就是平时办公的机器,统计不同排序算法排序过程中耗费的时间,这里的结果仅供参考。
模拟数据:随机得到1亿个范围在0到100000000内的整型元素构成数组,分别基于快速排序、普通归并排序、TimSort的排序算法得到耗时结果如下,单位ms。

通过测试验证结果来看,在当前数据集规模下,双轴快排DivalQuickSort表现优异。注:Java中TimSort主要运用引用类型的集合排序中,本次数据验证并未加入比较。
由于Java提供了很方便的排序API,所以在平时的需求使用过程中一般都是短短几行代码调用使用完整排序工作,这也是Java作为一门流行语言最基本的职责所在。当然也会导致我们开发者容易忽视其原理,不能够学习到里面的精髓。
文中一起了解学习了TimSort算法和DivalQuickSort的排序思想与java实现。作为基本排序实现被广泛的应用,肯定有其值得学习与借鉴的地方。可以得知工业用排序算法,通常都不是一种算法,而是根据特定条件下的多种算法混合而成,实际上平时很多使用的经典数据结构都不是一种类型或者一种方式,比如HashMap中随着数据量大小有链表与红黑树的转化,再比如Redis中的各种数据结构不都是一种实现。这些经典优秀的实现都用到了诸如此类的思想。
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr
我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢
我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只
我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA