归并的术语定义: 就是将两个有序的数组,归并为一个更大的有序数组。
归并排序优点:和前面那几种O(N²)的几种算法相比,归并算法 能够保证任意长度为N的数组排序所需的时间和 NlogN 成正比;缺点:它的主要缺点则是它需要额外的空间和N成正比。
我想:时间就是生命,生命是有限的,内存这玩意,和我们的生命来比,近似看作无限,毕竟内存作为宇宙物质我几乎就已经当作它是无限的(除非特殊情况) 。
算法思路:
我精心画了几张图,结合图文,方便你理解。
先分半,再归并,直到分半到一个元素后,无法再继续分半后,进行归并,归并的同时要保持元素的顺序。
深入归并的详细过程:
由于在左和右进行归并的过程中发现,只通过交换元素过程来完成归并比较复杂,所以大多时候人们都是通过开辟一块新的一块大小一样的临时空间的方法来解决这个问题。(ps:这里有空间增加的问题,但是空间和时间来重要性来比较,只要不是太过分,人们往往几乎都是喜欢选择时间。)。 我们来用 [ index ] 来代表各个指向变量追逐的元素 ,详细描述一下这个过程:
使用三个索引的变量 i , j 和 k 进行追踪索引变化,[ i ] , [ j ] ,[ k ] 指代被追踪索引位置上的元素 :
①初始 i 变量指向左半边开始位置,比较到mid结束;
int mid = lo + (hi - lo) / 2;
② j 变量 指向右半边开始位置,其索引从[mid+1] 开始到hi结束;
③ k 变量 用来接受 在 i 和 j 所指的元素都是当前循环 正在比较中的元素 , 规则 就是 把 i 和 j 指向的最小值,赋给 索引为 k 的位置 的空间。
比较完成后,参与赋值的元素的下标索引 ++, 继续指向旁边的下一个位置。
[ i ] 和 [ j ] 上的元素比较谁小,[i] 上的小,那么 [ i ] 上的元素就赋值给 [ k ],i++,k++ 同步递增,以保证比较过的地方都偏移一位,方便接下面下一次的新位置的比较。
同理,以上比较结果中是 [ j ] 上的元素小,那么把[ j ] 上的元素辅助给 [ k ] 。j++,k++ 同步递增,以保证比较过的地方都偏移一位,方便接着的下一次的新位置的比较。
在这一层,你会发现一个细节,这部分 k++ 是一个共有的移动条件,所以当用程序语言去描写这个过程的时候,把k++这一句 设计在for 循环的范围设定语句之后的那句,即 for括弧的第三句---规定变化方向幅度的哪句去描述这种变化是最恰当的。
把 i++ 和 j++ 设计在 循环体内 并 用 if...else 去描述 更加合适。
元素是有限的,不断次数增多,随着操作的深入,接下来我们就应该去思考,一些临界条件的考虑。
当 索引 [i] 超过了左半边的结尾,我们该怎么做 ? 此时,毫无疑问,[k] 只能接受 [j] 的元素;而同时,j 还要陪着 k 同步递增,所以 [k] = [j++] ,j++ 就是先取当前j的值,再++。
当 索引 [j] 超过了右半边的结尾,我们该怎么做 ? 此时,毫无疑问,[k] 只能接受 [i] 的元素;而同时,i 还要陪着 k 同步递增,所以 [k] = [i++] ,++ 妙用,先取当前 i 值,再和k一起递增。
在i 超过 mid 的时候,k全部取j 的值,不再需要进行比较逻辑;当j超过hi的时候也不用在进行比较逻辑;所以这些操作场景是互斥场景。
归并算法的实现:
实现部分,利用了递归的思想。sort 作为递归函数,内部 先排左半边(sort左),再排右半边(sort右),然后归并merge。 最小问题的条件为:if(hi<=lo) 及lo追上了hi,或者hi降低到了lo终止 sort。
package com.cosyit.offer.algorithms;
import java.util.Arrays;
public class Merge {
private static Comparable[] aux;
private static void sort(Comparable[] a) {
// 1. 开辟一个临时空间 aux 奥格zi尼亚瑞
aux = new Comparable[a.length];
//2.调用排序方法。
sort(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int lo, int hi) {
//临界条件。
if (hi <= lo) return;
//左半边最后一个元素。
int mid = lo + (hi - lo) / 2;
sort(a, lo, mid); //运用递归 排 左半边
sort(a, mid + 1, hi); //运用递归 排 右半边
//归并
merge(a, lo, mid, hi);
}
private static void merge(Comparable[] a, int lo, int mid, int hi) {
int i = lo, j = mid + 1;
//归并的时候,需要用到 aux 奥格zi尼亚瑞,这个临时空间。对此临时空间进行初始化。
for (int k = lo; k <= hi; k++) aux[k] = a[k];
for (int k = lo; k <= hi; k++) {
if (i > mid) a[k] = aux[j++];
else if (j > hi) a[k] = aux[i++];
else if(less(aux[j],aux[i])) a[k]= aux[j++];
else a[k]= aux[i++];
}
}
private static boolean less(Comparable a, Comparable b) {
return a.compareTo(b)<0;
}
public static void main(String[] args) {
Integer [] arr = {6,7,1,4,3,5,2,0};
sort(arr);
System.out.println(Arrays.toString(arr));
}
}
以上代码就是大体思路,其实还是有几处的优化的空间的。
- 第一处: 由于左右2部分都是有序后再去进行merge归并的,所以当左边最大已经比右边最小还小或相等的时候,就不要进行无谓的merge操作了。merge函数前,添加这样的代码。
if (lessOrEqual(a[mid ], a[mid+1])) return; 具体的优化代码如下:
if (lessOrEqual(a[mid ], a[mid+1]))
return;
for (int k = lo; k <= hi; k++) {
//
if (i > mid) a[k] = aux[j++];
else if (j > hi) a[k] = aux[i++];
else if(less(aux[j],aux[i])) a[k]= aux[j++];
else a[k]= aux[i++];
}
//为了方便,编写一个新的函数。
private static boolean lessOrEqual(Comparable a, Comparable b) {
return a.compareTo(b) <= 0;
}
- 第二处:很多算法都会使用递归函数,递归到底。目前我们的代码的临界条件是 递归到只有一个元素的时候,直接返回。我们可以在递归到元素个数非常少的时候,转为插入排序,以提高性能。因为2个原因:第一个原因就我们探讨的这个算法来说,当元素基数较多情况下,被排序多次后,当元素数量比较少的时候,每个部分的相对有序的概率比较大,插入排序更有优势。第二个原因,N²和NlogN来对比,当N的小到一定程度的时候,插入排序会比归并排序快一些。所以我们代码还可以修改为下面的模样。
private static void sort(Comparable[] a, int lo, int hi) { // if (hi <= lo) return; //临界条件。 //ps 优化二 if(hi-lo <=15){ insertionSort(a,lo,hi); return; } //左半边最后一个元素。 int mid = lo + (hi - lo) / 2; sort(a, lo, mid); //运用递归 排 左半边 sort(a, mid + 1, hi); //运用递归 排 右半边 //归并 merge(a, lo, mid, hi); }
由于添加了一个新的insertSort方法,固我把完整能跑的代码贴出来,供你参考:
package com.cosyit.offer.algorithms; import java.util.Arrays; public class Merge { private static Comparable[] aux; private static void sort(Comparable[] a) { // 1. 开辟一个临时空间 aux 奥格zi尼亚瑞 aux = new Comparable[a.length]; //2.调用排序方法。 sort(a, 0, a.length - 1); } private static void sort(Comparable[] a, int lo, int hi) { //临界条件。 // if (hi <= lo) return; //ps 优化二 if(hi-lo <=3){ insertionSort((Integer[]) a,lo,hi); return; } //左半边最后一个元素。 int mid = lo + (hi - lo) / 2; sort(a, lo, mid); //运用递归 排 左半边 sort(a, mid + 1, hi); //运用递归 排 右半边 //归并 merge(a, lo, mid, hi); } private static void merge(Comparable[] a, int lo, int mid, int hi) { int i = lo, j = mid + 1; //归并的时候,需要用到 aux 奥格zi尼亚瑞,这个临时空间。对此临时空间进行初始化。 for (int k = lo; k <= hi; k++) aux[k] = a[k]; //由于左右2部分都是有序后再去进行merge归并的,所以当左边最大已经比右边最小还小或相等的时候,就不要进行无谓的merge操作了。 if (lessOrEqual(a[mid ], a[mid+1])) return; //ps 优化1 for (int k = lo; k <= hi; k++) { if (i > mid) a[k] = aux[j++]; else if (j > hi) a[k] = aux[i++]; else if (less(aux[j], aux[i])) a[k] = aux[j++]; else a[k] = aux[i++]; } } private static boolean less(Comparable a, Comparable b) { return a.compareTo(b) < 0; } private static boolean lessOrEqual(Comparable a, Comparable b) { return a.compareTo(b) <= 0; } private static void insertionSort(Integer[] a, int l,int r) { for (Integer i = l+1; i <= r; i++) { Integer e = a[i]; //拿在手上的牌。 Integer j; //保持元素e 应该插入的位置。 for (j = i; j > l && less(e,a[j - 1] ) ; j--) { a[j] = a[j - 1]; } a[j] = e; } System.out.println(Arrays.toString(a)); } public static void main(String[] args) { Integer[] arr = {6, 7, 1, 4, 3, 5, 2, 0}; sort(arr); System.out.println(Arrays.toString(arr)); } }