归并排序
首先将数组分成两个(或两个以上)部分,分别进行排序,然后将这些有序的子数组归并起来。
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 将已有序的子数组合并,得到完全有序的数组;即先使每个子数组有序,再使子数组间有序。
归并排序最吸引人的是它能够保证任意长度为N的数组排序的时间和NlogN成正比。缺点是他需要的额外的空间也和N成正比。
树状图更加容易理解:
设n为树的层数。第k层的节点是一个大小为2^n-k的数组,在归并的时候,我们需要比较左右两个部分的数组,所以需要比较2^n-k次。又因为第k层有2^k个节点。所以总共需要比较2^n次。
而树的高度为lgN,也就是n,所以最终需要比较NlogN次。
代码如下:
public static void sort(Comparable[] a) {
arrayTemp = new Comparable[a.length];
sort1(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int begin, int end) {
if (begin >= end)
return;
int mid = (begin + end) / 2;
sort(a, begin, mid); //先排序左边
sort(a, mid + 1, end); // 排序右边
merge(a, begin, mid, end); // 归并
}
private static void merge(Comparable[] a, int low, int mid, int high) {
int j = low;
int k = mid + 1;
for (int i = low; i <= high; i++) {
arrayTemp[i] = a[i];
}
for (int i = low; i <= high; i++) {
if (j >= mid + 1) { // 如果左边没了
a[i] = arrayTemp[k++];
} else if (k >= high + 1) { // 右边没了
a[i] = arrayTemp[j++];
} else if (less(arrayTemp[j], arrayTemp[k])) { // 左边的比较小
a[i] = arrayTemp[j++];
} else { // 右边的比较小
a[i] = arrayTemp[k++];
}
}
}
改进1:对小规模子数组使用插入排序。当排序的时候,如果发现数组的规模已经比较小了,我们可以使用别的排序方法进行排序。对于小规模的数组,插入排序或者选择排序可能会比归并排序更加快。
改进2:再进行归并操作之前,我们可以比较左边子数组的最后一位a[mid]和右边子数组的第一位a[mid+1],如果发现a[mid]<a[mid+1]。那么数组已经是有序的了。不需要再进行归并。
改进代码:
public static void sort(Comparable[] a) {
arrayTemp = new Comparable[a.length];
sort1(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int begin, int end) {
// 改进1,数组规模小的时候,使用插入排序
if (end - begin <= 7) {
InsertSort.sort(a, begin, end + 1);
return;
}
int mid = (begin + end) / 2;
sort(a, begin, mid);
sort(a, mid + 1, end);
// 改进2
if (less(a[mid], a[mid+1])) return;
merge(a, begin, mid, end);
}
private static void merge(Comparable[] a, int low, int mid, int high) {
int j = low;
int k = mid + 1;
for (int i = low; i <= high; i++) {
arrayTemp[i] = a[i];
}
for (int i = low; i <= high; i++) {
if (j >= mid + 1) { // 如果左边没了
a[i] = arrayTemp[k++];
} else if (k >= high + 1) { // 右边没了
a[i] = arrayTemp[j++];
} else if (less(arrayTemp[j], arrayTemp[k])) { // 左边的比较小
a[i] = arrayTemp[j++];
} else { // 右边的比较小
a[i] = arrayTemp[k++];
}
}
}
我们来比较一下运行结果,顺便比较一下和希尔排序的速度:
public static void main(String[] args) {
final int NUM = 1000000;
Integer[] a1 = new Integer[NUM];
Integer[] a2 = new Integer[NUM];
Integer[] a3 = new Integer[NUM];
Integer[] a4 = new Integer[NUM];
for (int i = 0; i < NUM; i++) {
a1[i] = (int) (Math.random() * NUM);
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
}
long startTime;
long endTime;
startTime = System.currentTimeMillis(); // 获取开始时间
ShellSort.sort(a2);
assert isSorted(a2);
endTime = System.currentTimeMillis();
System.out.println("shell排序cost: " + (endTime - startTime) + " ms");
startTime = System.currentTimeMillis(); // 获取开始时间
MergeSort.sort1(a3);
assert isSorted(a3);
endTime = System.currentTimeMillis();
System.out.println("Merge排序cost: " + (endTime - startTime) + " ms");
startTime = System.currentTimeMillis(); // 获取开始时间
MergeSort.sort2(a4);
assert isSorted(a4);
endTime = System.currentTimeMillis();
System.out.println("Merge排序改良cost: " + (endTime - startTime) + " ms");
}
运行结果:
shell排序cost: 1281 ms
Merge排序cost: 423 ms
Merge排序改良cost: 397 ms
普遍来说,改良的Merge排序比之前的Merge排序来说要快。
而归并排序相比较于希尔排序,又有了一个很大的提升。数据量比较大的时候,提升就越明显了。