归并排序(Merging Sort)
归并排序是分治思想最典型的例子。归并排序中最基本的操作是“归并”,即将两个(2-路归并)或两个以上的有序数组组合成一个更大的有序数组。按照归并顺序的不同,归并排序可以分为自顶向下和自底向上两类。
基本思想:
要将一个数组排序,可以先(递归地)将它拆分成两半分别排序,然后将结果归并起来。或者先归并微型数组,然后再成对归并得到的子数组,直到最后整个数组归并在一起。
实现归并的一种直截了当的方法是将两个不同的有序数组归并到第三个数组中,也就是说需要额外的辅助空间。但是,当用归并对一个大数组排序时,我们需要很多次的归并操作,如果每次归并时都创建一个新数组来存储排序结果,数据量很大时,开销累积起来将大大影响效率。因此,原地归并成为一个不错的想法。
要想实现原地归并,我们可以采用声明一个数组成员变量,在进行排序时一次性分配内存空间,之后的归并操作就只需要对已有的数组进行操作。
我们首先来看一幅描述归并排序过程(自顶向下)的图:
可以看到,自顶向下的归并排序进行的操作主要就是对数组的拆分与合并。通过层层拆分得到单元素数组,天生有序,然后归并两个单元素数组得到一个较大的有序数组,接着再归并两个较大数组得到更大的一个有序数组,重复这个过程,最终归并便得到了一个排好序的数组。
此外,自底向上的图如下:
自底向上的归并排序相比自顶向下而言,采取了先归并小数组再归并大数组的方法,少了对数组进行拆分的步骤,所以需要的代码量更小。
算法实现:
自顶向下
/**
* 自顶向下的归并排序
* @param arr 待排序数组
*/
private static void topDownMergeSort(int[] arr) {
aux = new int[arr.length]; // 一次性分配内存空间。aux 为辅助数组
sort(arr, 0, arr.length - 1);
}
/**
* 归并排序
* @param arr 待排序数组
* @param low 初始下标
* @param mid 中值下标
* @param high 结束下标
*/
private static void sort(int[] arr, int lo, int hi) {
if (lo >= hi)
return;
int mid = lo + (hi - lo) / 2;
sort(arr, lo, mid); // 将左半边排序
sort(arr, mid + 1, hi); // 将右半边排序
merge(arr, lo, mid, hi); // 归并结果
}
/**
* 归并排序基本操作,将 arr[lo...mid] 和 arr[mid + 1...hi] 归并
* @param arr 待排序数组
* @param lo 初始下标
* @param mid 中间下标
* @param hi 结束下标
*/
private static void merge(int[] arr, int lo, int mid, int hi) {
int i = lo;
int j = mid + 1;
// // 每次创建一个新数组将成为归并排序运行时间的主要部分,大大降低归并排序效率。1 百万个数大约 2 分钟
// aux = new int[hi + 1];
for (int k = lo; k <= hi; k++)
aux[k] = arr[k]; // 使用辅助数组保存当前数组
// 进行归并操作,将两个数组归并到一个数组
for (int k = lo; k <= hi; k++) {
if (i > mid) // 左半边数组元素取完了取右半边的元素
arr[k] = aux[j++];
else if (j > hi) // 右半边数组元素取完了取左半边的元素
arr[k] = aux[i++];
else if (aux[j] < aux[i]) // 右半边数组元素小于左半边的,则取右半边的元素
arr[k] = aux[j++];
else // 左半边数组元素 <= 右半边的,则取左半边的元素
arr[k] = aux[i++];
}
}
自底向上
/**
* 自底向上的归并排序
* @param arr 待排序数组
*/
private static void bottomUpMergeSort(int[] arr) {
int length = arr.length;
aux = new int[length];
for (int sz = 1; sz < length; sz = sz + sz) // sz:子数组大小。 初始为 1,每次加倍
for (int lo = 0; lo < length - sz; lo += sz + sz) // lo:子数组索引
merge(arr, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, length - 1));
}
/**
* 归并排序基本操作,将 arr[lo...mid] 和 arr[mid + 1...hi] 归并
* @param arr 待排序数组
* @param lo 初始下标
* @param mid 中间下标
* @param hi 结束下标
*/
private static void merge(int[] arr, int lo, int mid, int hi) {
int i = lo;
int j = mid + 1;
// 每次创建一个新数组将成为归并排序运行时间的主要部分,大大降低归并排序效率。1 百万个数大约 2 分钟
aux = new int[hi + 1];
for (int k = lo; k <= hi; k++)
aux[k] = arr[k]; // 使用辅助数组保存当前数组
for (int k = lo; k <= hi; k++) {
if (i > mid) // 左半边取完了取右半边的
arr[k] = aux[j++];
else if (j > hi) // 右半边取完了取左半边的
arr[k] = aux[i++];
else if (aux[j] < aux[i]) // 右半边的小于左半边的,则取右半边的
arr[k] = aux[j++];
else // 左半边的小于等于右半边的,则取左半边的
arr[k] = aux[i++];
}
}
算法测试:
package algorithms;
import java.util.Random;
public class MergeSortTest {
public static void main(String[] args) {
int size = 100000000;
int limit = 100000000;
int[] arr = getRandomArray(size, limit);
System.out.println("=====归并排序=====");
printArray(arr);
long start = System.currentTimeMillis();
topDownMergeSort(arr);
// bottomUpMergeSort(arr);
long end = System.currentTimeMillis();
System.out.println(size + " 个数,耗时 " + (end - start) + " ms\n");
printArray(arr);
}
/**
* 自底向上的归并排序
* @param arr 待排序数组
*/
private static void bottomUpMergeSort(int[] arr) {
int length = arr.length;
aux = new int[length];
for (int sz = 1; sz < length; sz = sz + sz) // sz:子数组大小。 初始为 1,每次加倍
for (int lo = 0; lo < length - sz; lo += sz + sz) // lo:子数组索引
merge(arr, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, length - 1));
}
/**
* 自顶向下的归并排序
* @param arr 待排序数组
*/
private static void topDownMergeSort(int[] arr) {
aux = new int[arr.length]; // 一次性分配内存空间。aux 为辅助数组
sort(arr, 0, arr.length - 1);
}
/**
* 归并排序
* @param arr 待排序数组
* @param low 初始下标
* @param mid 中值下标
* @param high 结束下标
*/
private static void sort(int[] arr, int lo, int hi) {
if (lo >= hi)
return;
int mid = lo + (hi - lo) / 2;
sort(arr, lo, mid);
sort(arr, mid + 1, hi);
merge(arr, lo, mid, hi);
}
/**
* 归并排序基本操作,将 arr[lo...mid] 和 arr[mid + 1...hi] 归并
* @param arr 待排序数组
* @param lo 初始下标
* @param mid 中间下标
* @param hi 结束下标
*/
private static void merge(int[] arr, int lo, int mid, int hi) {
int i = lo;
int j = mid + 1;
// // 每次创建一个新数组将成为归并排序运行时间的主要部分,大大降低归并排序效率。1 百万个数大约 2 分钟
// aux = new int[hi + 1];
for (int k = lo; k <= hi; k++)
aux[k] = arr[k]; // 使用辅助数组保存当前数组
for (int k = lo; k <= hi; k++) {
if (i > mid) // 左半边取完了取右半边的
arr[k] = aux[j++];
else if (j > hi) // 右半边取完了取左半边的
arr[k] = aux[i++];
else if (aux[j] < aux[i]) // 右半边的小于左半边的,则取右半边的
arr[k] = aux[j++];
else // 左半边的小于等于右半边的,则取左半边的
arr[k] = aux[i++];
}
}
/**
* 获得一个随机整型数组
* @param size 数组大小
* @param limit 数组元素大小的上限
* @return 一个随机数组
*/
private static int[] getRandomArray(int size, int limit) {
int[] arr = new int[size];
Random random = new Random();
for (int i = 0; i < size; i++)
arr[i] = random.nextInt(limit);
return arr;
}
/**
* 打印数组,默认只打印前一百个
* @param arr 待打印数组
*/
private static void printArray(int[] arr) {
int length = arr.length;
int count = 1;
for (int i = 0; i < length; i++) {
System.out.printf("%-10d", arr[i]);
if (count++ % 10 == 0)
System.out.println();
if (count == 101 && count < length) {
System.out.print("......");
break;
}
}
System.out.println("\n");
}
private static int[] aux;
}