归并排序
“归并”的含义是将两个或者两个以上的有序表组合成一个新的有序表。假设待排序表含有n个记录,则可以看成n个有序的子表,每个子表长度为1,然后两两归并,得到 n/2 个长度为2或者1的有序表;再两两归并,直到合并成一个长度为n的有序表为止,这种排序方法称为 2-路归并排序。
递归的2-路归并排序算法是基于分治算法的 ,以O(NlogN)最坏情形时间运行,而所使用的比较次数几乎是最优的,他是递归算法一个好的实例。
分解:将含有n个元素的待排序表分成各含 n/2 个元素的子表,采用2-路归并排序算法对两个子表递归的进行排序
合并:合并两个已排序的子表得到排序结果
public class MergeSort {
private int[] arrB;
public void merge(int[] arrA, int low, int mid, int high) {
// 表A的两段A[low...mid]和A[mid+1...high]各自有序,将他们合并成一个有序表
// 将A中的所有元素复制到B中
/*for (int k = low; k < high; k++) {
arrB[k] = arrA[k];
}*/
arrB = arrA.clone();
// 比较B中的左右两段中的元素,将较小的值复制给A
int i = 0, j = 0, k = low;
for (i = low, j=mid; i<=mid-1 && j<=high; k++) {
if(arrB[i] <= arrB[j]) {
arrA[k] = arrB[i++];
}else {
arrA[k] = arrB[j++];
}
}
// 若第一个表未检测完,复制
while(i<mid) {
arrA[k++] = arrB[i++];
}
// 若第二个表未检测完,复制
while(j<high) {
arrA[k++] = arrB[j++];
}
}
public void mergeSort(int[] arrA, int low, int high) {
if(low < high) {
int mid = (low+high)/2; // 从中间划分两个字序列
mergeSort(arrA, low, mid); // 从左侧子序列进行递归排序
mergeSort(arrA, mid+1, high); // 从右侧子序列进行递归排序
merge(arrA, low, mid+1, high); // 归并
}
}
@Test
public void testSort() {
int [] arrA = {49, 38, 65, 97, 76, 13, 27};
arrB = new int[arrA.length];
mergeSort(arrA, 0, arrA.length-1);
System.out.println(Arrays.toString(arrA));
}
}
性能分析:
空间效率:在merge()操作中,辅助空间刚好要占用n个单元,所以归并排序的的空间复杂度为O(n)
时间效率:每一趟归并的时间复杂度为O(n),共需要进行 logN 趟排序,所以时间复杂度为 O(NlogN)
因为merge()操作不会改变相同关键字记录的相对次序,所以是一个稳定的排序方法
深入分析:
虽然归并排序的运行时间是 O(NlogN) ,但他有一个明显的问题,即合并两个已排序的表用到线性附加内存,在整个算法中还要花费将数据拷贝到临时数组再拷贝回来这些附加的工作,他明显减慢了排序的速度。
与其他的 O(NlogN) 排序算法比较,归并排序的运行时间严重依赖于比较元素和在数组(以及临时数组)中移动元素的相对开销。
例如:在java中,当执行一次泛型排序(使用Comparator)时,进行一次元素比较可能是昂贵的(因为比较可能不容易被内嵌,从而动态的调用开销可能会减慢执行的速度),但是移动元素则是省时的(因为他们是引用的赋值,而不是庞大对象的拷贝)。归并排序使用所有流行的排序算法中最少的比较次数,因此是使用java的通用排序算法中的上好选择。事实上,他就是标准java类库中泛型排序所使用的算法。