代码测试:
1、测试归并排序的有效性和时间复杂度
SortingUtil.testSortingAlgorithm(new MergeSort(), new GenerateRandomArrayStrategy(100_0000));
SortingUtil.testSortingAlgorithm(new MergeSort(), new GenerateRandomArrayStrategy(400_0000));
结论:几乎是线性的。我们传入的测试用例是一个百万级别的随机数组,我们发现,归并排序平均在 0.2 秒就完成了排序任务。
2、比较归并排序与插入排序,测试用例:随机生成的数组
SortingUtil.compareSortingAlgorithms(new GenerateRandomArrayStrategy(10_0000),
new InsertionSortOptimize(),
new MergeSort());
结论:即使是还没有优化过的归并排序,都比插入排序的优化版本要快。
3、比较归并排序与插入排序,测试用例:几乎有序的数组
SortingUtil.compareSortingAlgorithms(new GenerateNearlySortedArrayStrategy(10_0000, 0.99),
new InsertionSortOptimize(),
new MergeSort());
结论:依然还是归并排序要快。
归并排序的相关结论

这样做是有原因的:这里隐含的一个知识点是:
当子区间的长度是偶数的时候,位于中间的数其实有 2 个,而 int mid = (left + right) / 2; 因为除法运算符 / 是向下取整的,因此,它取到的是 2 个中间位置的数当中靠左边的那个数,我们不妨称它为左中位数。
你可以在纸上具体举几个例子计算一下。相信这一行代码为什么取的是左中位数并不难理解。
那么在子区间只有 2 个元素的时候,mid 取的就是这个子区间的第 1 个元素,相应的 mid + 1 是这个子区间的第 2 个元素,两个索引值都是有意义的。
如果你将递归处理的部分这样定义:
-
数组
[left, mid - 1]递归调用一次; -
数组
[mid , right]递归调用一次。
那就表示 mid 是当前子区间第 2 部分的第 1 个元素的索引。
而此时 mid - 1 就有数组下标越界的风险,还是当子区间只有 2 个元素的时候,mid 在这一行代码下,取的是第 1 个元素,mid - 1 就越界了,那么解决的办法很简单,在括号里加一个 1 即可。
因为递归调用的语义变了,后面归并两个有序子区间的代码也就要做相应的调整,那么有兴趣的同学不妨可以做这样一个练习,取右中位数,怎么把后面的代码都写对,相信并不难。
递归

- 我们要在待归并的两个数组上设置两个变量。一开始的时候
i和j分别指向待归并数组的首元素,然后比较它们所指向的元素,哪个元素更小,就把它的值复制到归并以后的数组,然后它向右移动一格;我们需要借助两个变量,一开始我们将变量放置在两个数组的起始位置,比较当前这两个变量所指的位置上的元素,哪个元素小,我们把这个元素复制到归并以后的数组中,然后这个变量向后移动一位,而这个变量之前的元素均已经归并完毕, - 重复这样的过程,直到
i和j都遍历到了两个待排序的数组的末尾。
暂时不用的东西。
假设两个待归并的数组分别是 [2, 3, 5, 7] 和 [1, 3, 6, 8],事实上,这个过程很像合并两叠已经按顺序码好序的扑克牌。

我们每次都只看这两个序列的第 1 个元素,选出它们当中较小的那个元素归并到最终它应该在的位置。
- 首先
2和1比较,1出列; - 接着
2和3*比较,2出列; - 接着
3和3*比较,3出列; - 接着
3*和5比较,3*出列; - 接下里出列的顺序依次是
5、6、7、8。
这个过程看起来非常简单。不过事实上,如果我们真正要在数组中做这样的操作,其实是比较费劲的一件事,因为我们每次要从数组的头部删除一个元素,然后把它后面的那些元素都向前移动一格,这样的操作是 的。

归并排序的优化
代码测试:
1、测试代码正确性
SortingUtil.testSortingAlgorithm(new MergeSortOptimize(), new GenerateRandomArrayStrategy(100_0000));
2、对比实验
SortingUtil.compareSortingAlgorithms(new GenerateRandomArrayStrategy(100_0000),
new MergeSort(),
new MergeSortOptimize());
结论:优化以后确实速度提升了。