前言
这篇文章本该发表于2018年4月份末,在排序算法:快速排序之后,但是不知道什么原因,这篇文章忘了在CSDN上发表,今天在看博客的时候突然发现,因此补上。
之前介绍过排序算法中效率很高的“快速排序”。本文将介绍另一个高效的排序算法——“归并排序”。
归并排序的主要思想是分治法。主要过程是:
1. 将n个元素从中间切开,分成两部分。(左边可能比右边多1个数)
2. 将步骤1分成的两部分,再分别进行递归分解。直到所有部分的元素个数都为1。
3. 从最底层开始逐步合并两个排好序的数列。
考虑一个问题,如何将两个有序数列合并成一个有序数列?
很简单,由于两个数列都已经有序,我们只需从两个数列的低位轮番拿出各自最小的数来PK就就行了,输的一方为小值,将这个值放入临时数列,然后输的一方继续拿出一个值来PK,直至有一方没有元素后,将另一方的所有元素依次接在临时数列后面即可。此时,临时数列为两个数列的有序合并。归并排序中的归并就是利用这种思想。对应的代码如下:
/** * 合并两个有序数列 * array[first]~array[mid]为第一组 * array[mid+1]~array[last]为第二组 * temp[]为存放两组比较结果的临时数组 */ private static void mergeArray(int array[], int first, int mid, int last, int temp[]) { int i = first, j = mid + 1; // i为第一组的起点, j为第二组的起点 int m = mid, n = last; // m为第一组的终点, n为第二组的终点 int k = 0; // k用于指向temp数组当前放到哪个位置 while (i <= m && j <= n) { // 将两个有序序列循环比较, 填入数组temp if (array[i] <= array[j]) temp[k++] = array[i++]; else temp[k++] = array[j++]; } while (i <= m) { // 如果比较完毕, 第一组还有数剩下, 则全部填入temp temp[k++] = array[i++]; } while (j <= n) {// 如果比较完毕, 第二组还有数剩下, 则全部填入temp temp[k++] = array[j++]; } for (i = 0; i < k; i++) {// 将排好序的数填回到array数组的对应位置 array[first + i] = temp[i]; } }
下面通过一个例子来看看归并排序是怎么工作的,原数组如下。
第一步:分解
首先将数组分解成两部分,即19、15、37为一组,12、25为一组,为了区分,我们起个名字叫“第一层”,如下图:
第二步:分解
继续分解,19、15为一组,37为一组,12为一组,25为一组,这四组为“第二层”,如下图:
第三步:分解
继续分解,此时只剩下19、15这一组可以分解,分解成19、15,这两组为“第三层”,如下图:
第四步:归并
由于所有组都已经分解成只有1个元素,开始进行归并,从“高层”开始归并,即先归并“第三层”,比较“第三层”两组元素,19 < 15,因此将15排在19前面,由于已经没有元素,结束此次归并,如下图:
第五步:归并
继续归并,此次归并“第二层”,这一层有4个组,进行两两比较。首先,比较15、19和37:15 < 37,所以15放第一个位置,接着比较19和37,19 < 37,所以19放第二个位置,此时第一组15、19已经没有元素,于是将37填入15和19之后。接着比较:12和25:12 < 25,所以12放第一个位置,由于第一组12已经没有元素,于是将25填入12之后。归并的结果如下:
第六步:归并
继续归并,此次归并“第一层”,这一组有2个组,第一组:15、19、37,第二组:12、25。同样的,取两组的第1个数比较:15 > 12,所以12放第1个位置;接着取第二组的第2个数比较:15 < 25,所以15放第2个位置;接着取第一组的第2个数比较:19 < 25,所以19放第3个位置;接着取第一组的第3个数比较:37 > 25,所以25放第4个位置;由于第二组已经没有元素,所以37自然归入第5个位置。此时,归并结束,最终数组如下。
整个例子的完整过程图如下:
用一张动图来展示整个归并排序的过程。图中,开始每个柱子的颜色都不同,代表所有数字被分解成了各自一组,红色的柱子代表归并后的数列。
合并的代码在开头已经给过了,分解的代码较简单,就是递归调用归并方法,最后调用合并方法,最终代码如下。
public class MergeSort { public static void mergeSort(int[] array) { if (array == null || array.length == 0) return; int[] temp = new int[array.length]; mergeSort(array, 0, array.length - 1, temp); } // 归并 private static void mergeSort(int array[], int first, int last, int temp[]) { if (first < last) { int mid = (first + last) / 2; mergeSort(array, first, mid, temp); // 递归归并左边元素 mergeSort(array, mid + 1, last, temp); // 递归归并右边元素 mergeArray(array, first, mid, last, temp); // 再将二个有序数列合并 } } /** * 合并两个有序数列 * array[first]~array[mid]为第一组 * array[mid+1]~array[last]为第二组 * temp[]为存放两组比较结果的临时数组 */ private static void mergeArray(int array[], int first, int mid, int last, int temp[]) { int i = first, j = mid + 1; // i为第一组的起点, j为第二组的起点 int m = mid, n = last; // m为第一组的终点, n为第二组的终点 int k = 0; // k用于指向temp数组当前放到哪个位置 while (i <= m && j <= n) { // 将两个有序序列循环比较, 填入数组temp if (array[i] <= array[j]) temp[k++] = array[i++]; else temp[k++] = array[j++]; } while (i <= m) { // 如果比较完毕, 第一组还有数剩下, 则全部填入temp temp[k++] = array[i++]; } while (j <= n) {// 如果比较完毕, 第二组还有数剩下, 则全部填入temp temp[k++] = array[j++]; } for (i = 0; i < k; i++) {// 将排好序的数填回到array数组的对应位置 array[first + i] = temp[i]; } } }
归并排序的时间复杂度为O(nlogn),推导过程较复杂,在此不多赘述。
从代码中可以看出,归并排序需要一个跟待排序数组同等空间的临时数组,因此,使用归并排序时需要考虑是否有空间上的限制。如果没有空间上的限制,归并排序是一个不错的选择。在本人的电脑测试,100万的随机数字,归并排序大约耗时150毫秒。