timsort java_08 排序四:如何实现一个通用的排序算法?(附Java TimSort)

前面几篇讲过了几种常用的排序算法,这篇就是考虑如何实现一个通用的排序算法。

首先回顾一下排序算法的一些性能相关的信息,如图。

其中,线性排序的时间复杂度比较低,但是适用场景较为特殊,所以无法用于通用的排序算法。

如果是针对于小规模数据进行排序,可以选择时间复杂度为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.comzhihu-card-default.svg淤白:06 排序二:希尔、归并、快速​zhuanlan.zhihu.comzhihu-card-default.svg淤白:07 排序三:桶、计数、基数​zhuanlan.zhihu.comzhihu-card-default.svg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值