前面几篇讲过了几种常用的排序算法,这篇就是考虑如何实现一个通用的排序算法。
首先回顾一下排序算法的一些性能相关的信息,如图。
其中,线性排序的时间复杂度比较低,但是适用场景较为特殊,所以无法用于通用的排序算法。
如果是针对于小规模数据进行排序,可以选择时间复杂度为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 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; // Arrays of size 0 and 1 are always sorted
// 待排序序列大小小于32,直接走binarySort if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
binarySort(a, lo, hi, lo + initRunLen, c);
return;
}
TimSort 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;
}
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 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; // 不进行merge操作 }
}
}
当要合并时,由于要合并的两个序列已经有序,所以合并的时候,也有一处优化。假设两个序列是run1和run2,先用gallopRight找到run2首元素在run1中的位置(合并时可忽略run1中在此位置之前的元素),然后用gallopLeft找出run1尾元素在run2中的位置(合并时可忽略run2中在此位置之后的元素)。然后根据位置的大小关系执行mergeLo或者mergeHi。
private void mergeAt(int i) {
assert stackSize >= 2;
assert i >= 0;
assert i == stackSize - 2 || i == stackSize - 3;
int base1 = runBase[i];
int len1 = runLen[i];
int base2 = runBase[i + 1];
int len2 = runLen[i + 1];
assert len1 > 0 && len2 > 0;
assert base1 + len1 == base2;
runLen[i] = len1 + len2;
if (i == stackSize - 3) {
runBase[i + 1] = runBase[i + 2];
runLen[i + 1] = runLen[i + 2];
}
stackSize--;
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;
if (len1 <= len2)
mergeLo(base1, len1, base2, len2);
else
mergeHi(base1, len1, base2, len2);
}
排序系列淤白:05 排序一:冒泡、插入、选择zhuanlan.zhihu.com淤白:06 排序二:希尔、归并、快速zhuanlan.zhihu.com淤白:07 排序三:桶、计数、基数zhuanlan.zhihu.com