如果有两个数组已经有序,那么可以把这两个数组归并为更大的一个有序数组。归并排序便是建立在这一基础上。要将一个数组排序,可以将它划分为两个子数组分别排序,然后将结果归并,使得整体有序。子数组的排序同样采用这样的方法排序,这个过程是递归的。
下面举例说明,假如要对数组 a={2,1,3,5,2,3}
进行排序,那么把数组划分为 {2,1,3}
和 {5,2,3}
两个子数组,这两个子数组排序后变为 {1,2,3}
和 {2,3,5}
,然后对这两个数组进行归并操作便得到最终的有序数组。代码实现如下:
void sort(int[] a) { int[] aux = new int[a.length]; //辅助数组 mergeSort(a, 0, a.length - 1, aux); } void mergeSort(int[] a, int lo, int hi, int[] aux) { if (hi <= lo) return; int mid = lo + (hi - lo) / 2; mergeSort(a, lo, mid, aux); mergeSort(a, mid + 1, hi, aux); merge(a, lo, mid, hi, aux); } void merge(int[] a, int lo, int mid, int hi, int[] aux) { int i = lo, j = mid + 1; for (int k = lo; k <= hi; k++) { aux[k] = a[k]; } for (int k = lo; k <= hi; k++) { if (i > mid) a[k] = aux[j++]; else if (j > hi) a[k] = aux[i++]; else if (aux[i] <= aux[j]) a[k] = aux[i++]; else a[k] = aux[j++]; } }
对于归并排序有几点说明:
归并排序的时间复杂度是 O(NLogN)
,空间复杂度是 O(N)
。
辅助数组是一个共用的数组。如果在每个归并的过程中都申请一个临时数组会造成比较大的时间开销。
归并的过程需要将元素复制到辅助数组,再从辅助数组排序复制回原数组,会拖慢排序速度。
归并排序有以下几点优化方法:
和快速排序一样,对于小数组可以使用插入排序或者选择排序,避免递归调用。
在 merge()
调用之前,可以判断一下 a[mid]
是否小于等于 a[mid+1]
。如果是的话那么就不用归并了,数组已经是有序的。原因很简单,既然两个子数组已经有序了,那么 a[mid]
是第一个子数组的最大值, a[mid+1]
是第二个子数组的最小值。当 a[mid]<=a[mid+1]
时,数组整体有序。
为了节省将元素复制到辅助数组作用的时间,可以在递归调用的每个层次交换原始数组与辅助数组的角色。
在 merge()
方法中的归并过程需要判断 i
和 j
是否已经越界,即某半边已经用尽。可以用另一种方式,去掉检测是否某半边已经用尽的代码。具体步骤是将数组 a[]
的后半部分以降序的方式复制到 aux[]
,然后从两端归并。对于数组 {1,2,3}
和 {2,3,5}
,第一个子数组照常复制,第二个则从后往前复制,最终 aux[]
中的元素为 {1,2,3,5,3,2}
。这种方法的缺点是使得归并排序变为 不稳定 排序。代码实现如下:
void merge(int[] a, int lo, int mid, int hi, int[] aux) { for (int k = lo; k <= mid; k++) { aux[k] = a[k]; } for (int k = mid + 1;k <= hi; k++) { aux[k] = a[hi - k + mid + 1]; } int i = lo, j = hi; //从两端往中间 for (int k = lo; k <= hi; k++) if (aux[i] <= aux[j]) a[k] = aux[i++]; else a[k] = aux[j--]; }
在上面的实现中,相当于将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。将一个大的数组的排序划分为小数组的排序是自顶向下的排序。还有一种实现是自底向上的排序,即先两两归并,然后四四归并......代码实现如下:
void sort(int[] a) { int N = a.length; int[] aux = new int[N]; for (int sz = 1; sz < N; sz += sz) { for (int lo = 0; lo < N - sz; lo += sz + sz) { //在每轮归并中,最后一次归并的第二个子数组可能比第一个子数组要小 merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1), aux); } } }