原文链接:https://jiang-hao.com/articles/2020/algorithms-algorithms-merge-sort.html
文章目录
算法介绍
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
-
自上而下的递归:它从树的顶端开始,然后向下操作,每次操作都问同样的问题(我需要做什么来排序这个数组?)并回答它(分成两个子数组,进行递归调用,合并结果),直到我们到达树的底部。
-
自下而上的迭代:不需要递归。它直接从树的底部开始,然后通过遍历这些片段再将它们合并起来。
在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
归并排序分为三个过程:
- 将数列划分为两部分(在均匀划分时时间复杂度为 );
- 递归地分别对两个子序列进行归并排序;
- 合并两个子序列。
不难发现,归并排序的核心是如何合并两个子序列,前两步都很好实现。
其实合并的时候也不难操作。注意到两个子序列在第二步中已经保证了都是有序的了,第三步中实际上是想要把两个 有序 的序列合并起来。
算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
代码实现
数组实现时间复杂度O(NlogN),空间复杂度O(N)
递归实现一:每次归并时都创建一个辅助数组
public static int[] sort(int[] nums) {
// 对数组进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(nums, nums.length);
if (arr.length<2) return arr;
int middle = (int) Math.floor(arr.length >> 1);
int[] left = Arrays.copyOfRange(arr, 0, middle);
int[] right = Arrays.copyOfRange(arr, middle, arr.length);
return merge(sort(left), sort(right));
}
public static int[] merge(int[] left, int[] right) {
// 创建一个辅助数组存储归并结果
int[] result = new int[left.length+right.length];
int i=0, j=0;
while (i+j < result.length) {
// 右侧数组全都转存完时,直接将左侧数组剩余的元素转存到结果数组
if (j==right.length) {
result[i+j] = left[i++];
}
// 左侧数组全都转存完时,直接将右侧数组剩余的元素转存到结果数组
else if (i==left.length) {
result[i+j] = right[j++];
}
// 否则,将两个子数组当前元素中较小的那个转存到结果数组中
else result[i+j] = left[i]<=right[j]? left[i++]: right[j++];
}
return result;
}
力扣运行结果:
执行用时:10 ms, 在所有 Java 提交中击败了30.97%的用户
内存消耗:44.2 MB, 在所有 Java 提交中击败了99.55%的用户
提交时间 | 提交结果 | 运行时间 | 内存消耗 | 语言 |
---|---|---|---|---|
几秒前 | 通过 | 10 ms | 43.8 MB |