前面几篇讲过了几种常用的排序算法,这篇就是考虑如何实现一个通用的排序算法。
首先回顾一下排序算法的一些性能相关的信息,如图。
![74de53332001595450c44f1da163152c.png](https://i-blog.csdnimg.cn/blog_migrate/38d2ceeda8f3846be787a7ac38a17075.png)
其中,线性排序的时间复杂度比较低,但是适用场景较为特殊,所以无法用于通用的排序算法。
如果是针对于小规模数据进行排序,可以选择时间复杂度为O(n^2)的排序算法;但如果对大规模的数据进行排序,还是时间复杂度O(nlogn)的更为高效。所以,为了兼顾任意规模数据的排序,一般选择时间复杂度为O(nlogn)的排序算法。
时间复杂度是O(nlogn)的排序算法不止一个,有前面说过的归并排序、快速排序,还有堆排序(等分享二叉树的时候再聊堆排序),这几个在编程语言中都有对应的排序函数实现。
以Java为例,其有基于快速排序的DualPivotQuicksort和基于归并排序的TimSort(以及ComparableTimSort,两者的主要区别在于是否使用自定义的比较器)。
下面聊一下TimSort的实现思路和代码。
TimSort的实现思路
这是一种基于归并排序和插入排序的混合排序算法,不过其对归并排序做了大量优化。
当待排序序列元素少于32个时,会使用binarySort,其基于二分查找实现了一个快速的插入排序算法。
当待排序序列元素大于等于32个时,就是真正的TimSort逻辑了。
- 选出minRun的大小,用于后续将待排序数组的分块。
- 找出初始的一组升序数据,countRunAndMakeAscending会找出一个runLen,代表找出的有序数据的长度。(如果寻找过程中发现的是一个降序数组,会进行reverse操作,保证升序)。
- 如果2中找到的runLen小于minRun,会利用binarySort对初始有序数组进行扩展,扩展后,保证仍有序。
- 进行入栈操作,分别存入有序数组起始下标、长度,便于后面的merge操作。
- 对栈中的多个数组进行merge操作。
- 重复2 ~ 5步骤,直到待排序数组中的数据排序完。
- 最终处理,如果此时仍有数组未merge,则进行merge,直到栈中数据都合并到一起。
代码
static
merge操作时, 只对相邻块进行merge。
假设X、Y、Z为三个相邻块。
当栈中只有两个块时,if (X <= Y),将X和Y进行merge。
当栈中块数大于等于3时,if (X <= Y + Z)时,如果X < Z,merge X和Y,否则合并Y和Z。
直到栈中块数为1,或者同时满足X > Y + Z和Y > Z,本轮操作结束。
private
当要合并时,由于要合并的两个序列已经有序,所以合并的时候,也有一处优化。假设两个序列是run1和run2,先用gallopRight找到run2首元素在run1中的位置(合并时可忽略run1中在此位置之前的元素),然后用gallopLeft找出run1尾元素在run2中的位置(合并时可忽略run2中在此位置之后的元素)。然后根据位置的大小关系执行mergeLo或者mergeHi。
private