归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
一、分治法思想
二、Java代码实现
2.1、自顶向下的归并排序
第一种归并排序被称为自顶向下的归并排序,这也是最经典的版本,它基于分治法,即将大数组拆分为左右两个小数组【小数组再次拆分为左右两个小数组直到不可拆分】,然后调用归并方法将已经拆分好并且有序的小数组归并成大数组即完成了归并排序。
public class MergeSort3 {
public static void main(String[] args) {
int[] a = {32,31,30,29,28,1,100};
//辅助数组
int[] aux = new int[a.length];
sort(a, aux,0, a.length - 1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+"\t");
}
}
public static void sort(int[] a, int[] aux, int left, int right) {
//将数组left...right排序
if (right <= left) {
return;
}
//中间划分点
int middle = left + (right - left)/2;
//将左半边排序
sort(a, aux, left, middle);
//将右半边排序
sort(a, aux, middle + 1, right);
//归并
merge(a, aux, left, middle, right);
}
public static void merge(int[] a, int[] aux, int left, int middle, int right) {
int index1 = left;//临时左数组最左边
int index2 = middle + 1;//临时右数组最左边
int i = left;//实际数组最左边
//将a[left...right]复制到辅助空间aux[left...right]
for (int k = left; k <= right; k++) {
aux[k] = a[k];
}
//将aux[left...right]按由小到大归并到a[left...right]中
while (index1 <= middle && index2 <= right) {
//两个数组的左指针都尚未越出当前数组范围(以mid和hi为最大点)
if (aux[index1] <= aux[index2]) {
//index1指向的临时数据更小,将其放入a的i指向的位置中,i向右移动,左数组的左指针向右移动,继续对比右数组的左指针
a[i++] = aux[index1++];
} else {
//index2指向的临时数据更小,将其放入a的i指向的位置中,i向右移动,右数组的做指针向右移动,继续对比左数组的左指针
a[i++] = aux[index2++];
}
}
//不符合上面的条件,说明左数组左指针已经移动到端点或者右数组的指针已经移动到端点
while (index1 <= middle) {
//右数组已经移动到端点,左数组尚未移动到端点,对左数组直接复制(已经有序)
a[i++] = aux[index1++];
}
while (index2 <= right) {
//左数组已经移动到端点,右数组尚未移动到端点,对右数组直接复制(已经有序)
a[i++] = aux[index2++];
}
}
}
归并排序中最重要的方法是merge方法,而sort方法的作用是组织拆分左右数组。merge方法中的第一个for循环的作用是将待排数组的数据复制到临时辅助数组aux中,并且在上文定义了i和index1、index2,其中index1和index2指向的是临时数组的左半边最左边的数据和右半边最左边的数据,即[index1=left,index2=middle+1],而i的作用是指向真实数组的最左边。
后面的三个while循环是这个方法的难点,它通过比较index1和index2的大小去决定i此时应该为哪个值,当其不满足第一个while循环的条件时,说明左数组左指针已经移动到端点或者右数组的指针已经移动到端点,这时需要根据两个不同的情况去为真实数组赋值,以下为图示:
2.2、自底向上的归并排序
这种情况用得比较少,这里不再赘诉。
public static void sort(int[] a){
int N = a.length;
aux = new Comparable[a.length];
for (int size = 1; size < N; size = size+size) {
for(int lo = 0; lo < N-size; lo += size+size){
merge(a, lo, lo+size-1, Math.min(lo+size+size-1, N-1));
}
}
}
三、改进
归并排序和快锁排序一样依赖于递归和分治,这在大数组的情况下性能优异,归并排序的时间复杂度为0(nlogn),但是当数组比较小的时候,归并排序由于大量的递归调用以及需要创建空间复杂度为0(n)的辅助空间,性能或许比插入排序或者选择排序低。所以这时可以有两种选择:
3.1、数据量小
当数据量小到一定范围时可以选择插入排序:
public static void sort(int[] a, int[] aux, int left, int right) {
//数据范围,视情况而定
int N = 100;
if (right - left <= N) {
//改用插入排序
InsertSort(a);
return;
}
//中间划分点
int middle = left + (right - left)/2;
//将左半边排序
sort(a, aux, left, middle);
//将右半边排序
sort(a, aux, middle + 1, right);
//归并
merge(a, aux, left, middle, right);
}
3.2、左右数组已经完全排好序
检测待归并的两个子数组是否已经有序,如果已经有序则直接复制进a中。