简介
MergeSort对已经反向排好序的输入时复杂度为O(n^2),而timsort就是针对这种情况,对MergeSort进行优化而产生的,平均复杂度为n*O(log n),最好的情况为O(n),最坏情况n*O(log n)。并且TimSort是一种稳定性排序。思想是先对待排序列进行分区,然后再对分区进行合并,看起来和MergeSort步骤一样,但是其中有一些针对反向和大规模数据的优化处理。
步骤
分区
分区的思想是扫描一次数组,把连续正序列(如果是升序排序,那么正序列就是升序序列),或者【严格】(保证排序算法的稳定性)的反序列做为一个分区(run),如果是反序列,把分区里的元素反转一下。 例如
1,2,3,6,4,5,8,6,4 划分分区结果为
[1,2,3,6],[4,5,8],[6,4]
然后反转反序列
[1,2,3,6],[4,5,8],[4,6]
合并
考虑一个极端的例子,比如分区的长度分别为 10000,10,1000,10,10,我们当然希望是先让10个10合并成20, 20和1000合并成1020如此下去, 如果从从左往右顺序合并的话,每次都用到10000这个数组和去小的数组合并,代价太大了。所以我们可以用一个策略来优化合并的顺序。
实例
以java中的ComparableTimSort.sort()为例子, 用了一个run stack来确定是否应该合并,
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
小于MIN_MERGE(32)的排序,分区后直接用二分插入排序
int minRun = minRunLength(nRemaining);
do {
//找出下一个分区的起始位置,同时也对反向序列做了翻转处理
int runLen = countRunAndMakeAscending(a, lo, hi);
//保证run stack中的run的都大于minRun ,如果当前分区太小,就从后面取出元素补足
if (runLen < minRun) {
int force = nRemaining <= minRun ? nRemaining : minRun;
binarySort(a, lo, lo + force, lo + runLen);
runLen = force;
}
//把run放入 run stack中
ts.pushRun(lo, runLen);
//判断是否应该合并,i是从栈顶开始的,知道不能合并为止
//1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1]
//2. runLen[i - 2] > runLen[i - 1]
ts.mergeCollapse();
lo += runLen;
nRemaining -= runLen;
} while (nRemaining != 0);
// Merge all remaining runs to complete sort
assert lo == hi;
//合并剩下的run
ts.mergeForceCollapse();
assert ts.stackSize == 1;
在看里面的一个比较重要的函数
/**
* 如果后2个run的长度加起来比前面一个长,则使用中间位置的run和前后长度更短的run一个合并
* 如果后2个run的长度加起来比前面一个短,则把后面2个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
}
}
}
时间: 2024-11-03 06:21:31