归并排序(merge sort)的基本思想:
归并排序使用的是分治的思想,分治(devide and conquer),字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
递归的思想在某种程度上来说和分治的思想不谋而合,即函数直接或间接调用自身,把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解。所以说分治算法往往都可以使用函数递归来实现。
递归的能力在于用有限的语句来定义对象的无限集合,代码的表达力很强,写起来简洁。但是函数的递归层次太多,空间复杂度很大,容易爆栈,函数调用耗时很高,可能会存在大量的重复计算,且调试起来特别麻烦。其实任何递归都能转换成非递归,使用递归往往可能是你对原问题的求解思路和过程没有思考清楚。不过非递归的形式可能不是很好理解,尤其对于像hanoi塔,递归形式才是最合理的算法。我们往往对于递归的算法,采用递归的分析,非递归的实现。
对于归并排序,这里给出了递归和非递归两种实现方式。对于归并排序,不同人实现的具体细节可能是不相同的,但解题的思路都是相似的。
递归的实现方式:
//归并排序
public static void mergeSort(int[] a){
//排序的数组不能为空且至少要有两个元素
if(a == null || a.length < 2){
return;
}
int left = 0, right = a.length - 1;
solve(a,left,right); //对区间[left,right]也就是[0,a.length - 1]中的元素进行排序
}
public static void solve(int[] a,int left,int right){
//当left>=right时,说明区间已不可再分;
if(left >= right){
return;
}
int mid = left + ((right - left) >> 1);
//对区间[left,mid]中的元素进行排序
solve(a,left,mid);
//对区间[mid + 1,right]中的元素进行排序
solve(a,mid + 1,right);
//将区间[left,mid]中的元素和区间[mid + 1,right]中的元素进行合并
merge(a, left, mid,mid + 1,right);
}
//合并区间
public static void merge(int[] a,int first1,int last1,int first2,int last2){
//创建一个临时的数组,保存第一个区间中的元素。
int[] tempArray1 = new int[last1 - first1 + 1];
System.arraycopy(a, first1, tempArray1, 0, last1 - first1 + 1);
//创建一个临时的数组,保存第二个区间中的元素。
int[] tempArray2 = new int[last2 - first2 + 1];
System.arraycopy(a, first2, tempArray2, 0, last2 - first2 + 1);
//index用于记录将两个区间中的最小元素同步回原数组时的下标
int index = first1,min1 = 0,min2 = 0;
//只要有一个区间中的元素全部同步回原数组,就结束循环
while (min1 < tempArray1.length && min2<tempArray2.length){
/*
当第一个区间的最小元素小于第二个区间的最小元素时,将该元素同步回原数组,该区间的最小元素下标后移,
用于记录原数组同步的位置的下标也后移。
*/
if(tempArray1[min1] <= tempArray2[min2]){
a[index] = tempArray1[min1];
index++;
min1++;
}else {
/*
当第二个区间的最小元素小于第一个区间的最小元素时,将该元素同步回原数组,该区间的最小元素下标后移,
用于记录原数组同步的位置的下标也后移。
*/
a[index] = tempArray2[min2];
index++;
min2++;
}
}
//若有一个区间全部同步回原数组,另一个区间可能没同步完全。这两个循环就是将可能剩余的元素全部同步回原数组。
while (min2 < tempArray2.length){
a[index] = tempArray2[min2];
index++;
min2++;
}
while (min1 < tempArray1.length){
a[index] = tempArray1[min1];
index++;
min1++;
}
}
上述这种实现方式每次合并的时候都需要创建两个原数组的副本。其实也可以只创建一个,但是那样对于区间边界的判断可能有些复杂。创建副本这里使用的方法是:1.先创建一个新数组。2.调用System给我们提供的一个拷贝数组的方法,帮我们复制数组的元素。其实还有更好的选择,博主大大后来才想到Arrays工具类也提供了一些拷贝数组元素的方法,比如Arrays.copyOfRange()、Arrays.copyOf()。当然也可以不使用Java 给我们提供好的API,自己可以重新编写复制数组元素的代码。因为每次合并都要创建数组,耗时较大,因此我们可以在这点做些优化,即开始排序的时候就先创建一个原数组的副本,下面就是这种方式的实现代码。
递归形式的改进:
//归并排序,将数组中范围从fromIndex(包括)到toIndex(不包括)中的目标序列进行升序排序
public static void mergeSort(int[] a,int fromIndex,int toIndex){
//排序的数组不能为空,目标序列必须在数组的范围内,且目标序列至少要有两个元素
if(a == null || fromIndex < 0 || toIndex > a.length || (toIndex - fromIndex) < 1){
return;
}
//创建好一个原数组的拷贝,并作为参数,传递进去
int[] copy = Arrays.copyOf(a,a.length);
mergeSort(a,copy,fromIndex