最近在看谢路云翻译的《算法》,这本书真是相当不错,值得好好研读。阅读之余也试着做每节后面的习题。这几天看到那几个基础的排序,归并排序那一章节后面有这样的一到习题:
改进:实现前面所述的对归并排序的三项改进,加快小数组的排序速度,检测数组是否已经有序以及通过在地柜中交换参数来避免数组复制。
书中提到的三项对归并排序的改进如下:1.对小规模子数组使用插入排序;2.测试数组是否已经有序;3.不将元素复制到辅助数组
一、文中指出使用插入排序处理小规模的子数组(比如长度小于15)一般可以将归并排序的运行时间缩短10-%15%;
二、在调用合并方法时,首先做个判断,看array[mid]是否小于array[mid+1],如果该条件成立,则认为数组已经是有序的并跳过合并方法(merge());
三、归并排序方法中需要一个辅助数组aux[ ],在每进行一次合并操作时,都要将aux复制一下原数组,使得aux保持与部分元素发生改变的原数组保持同样的状态,这样基于最新的状态,再对数组元素进行排序。但是即便是简单的复制操作对于规模较大的数组而言,也是一种比较大的开销。所以我们试着可以节省将数组元素复制到用于归并的辅助数组所用的时间。
下图是演示图:
下面是我试着写的代码,思路是跟着上面的图,刚开始接触算法,对第三个改进点理解的不是很透彻,所以对于第三个改进没有实现。
package merge_sort;
import sort_test.ShellSort;
/**
* 归并排序改进
*/
public class MergeSort {
private static int[] aux;
//局部插入排序
private static void localInsertSort(int[] array,int from, int end){
for(int i=from; i<=end; i++)
for(int j=i; j>from; j--){
if(array[j]<array[j-1]){
//exchange(arr[j],arr[j-1])
int mid = array[j-1];
array[j-1] = array[j];
array[j] = mid;
}
}
}
//归并排序
private static void mergeSort(int[] arr,int low,int high){
int mid = (low+high)/2;
int i = low, j=mid + 1;
//保持aux与arr同步
for(int m=0; m<arr.length; m++){
aux[m] = arr[m];
}
for(int k=low; k<=high; k++){
//左边子序列排完,就取右边子序列的数值
if(i>mid) arr[k] = aux[j++];
//右边子序列排完,就取左边子序列的数值
else if(j>high) arr[k] = aux[i++];
//左右子序列都有数时就相互比较
else if(aux[j]<aux[i]) arr[k] = aux[j++];
else arr[k] = aux[i++];
}
}
static int[] bound = new int[40];
static int num = 0;
//优化的归并排序
private static void sort(int[] arr, int low, int high){
if(low>=high) return;
int mid = (high+low)/2;
aux = new int[arr.length];
//长度小于15的子数组就不再进行减半<span style="white-space:pre"> </span>
<span style="font-family: Arial, Helvetica, sans-serif;"><span style="white-space:pre"> </span>if(high-low > 15){</span>
sort(arr,low,mid);
sort(arr,mid+1,high);
}else{
System.out.println(low+"-"+high +" insertSort run");
localInsertSort(arr,low,high);
}
bound[num++] = low;
bound[num++] = high;
if(num>2 && (bound[num-2]<bound[num-3])){
if(arr[mid]>=arr[mid+1]){
mergeSort(arr,bound[num-2],bound[num-1]);
System.out.println(bound[num-2]+"-"+bound[num-1]+" mergeSort run");
}
}
}
//正常的归并排序
private static void oneSort(int[] arr,int low, int high){
if(low>=high) return;
int mid = (high+low)/2;
aux = new int[arr.length];
oneSort(arr,low,mid);
oneSort(arr,mid+1,high);
mergeSort(arr,low,high);
}
public static long getTime(){
return System.currentTimeMillis();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] arr;
//初始化arr
arr = ShellSort.init();
long t1 = getTime();
sort(arr,0,arr.length-1);
long t2 = getTime();
System.out.println("advanced MergeSort costs "+(float)(t2-t1) +" second");
long t3 = getTime();
oneSort(arr,0, arr.length-1);
long t4 = getTime();
System.out.println("normal MergeSort costs "+(float)(t4-t3) +" second");
System.out.println(
"advanced mergeSort is "+ (float)(t4-t3)/(t2-t1)+" times faster than normal mergeSort");
}
}
说下写的过程中遇到的问题:
子数组之间的合并。我测试时使用了一个100长度的数组,首先是对整个数组由顶向下的递归分割。以下图为例,图中的数字代表的是数组元素的下标。分割得到8个长度小于15的子数组:0-12、13-24、25-37、 38-49、 50-62、 63-74、 75-86、 87-99。
在分别完成0-12、 13-24的插入排序后,将这两个子数组合并成0-24。 接下来要如何保证0-24 不和 25-37合并,我首先想到的是子数组的规模,即规模相当的子数组进行合并。要计算子数组的规模,我想到的就是创建一个数组(暂且称这个数组为下标数组),记录子数组的首尾两端的下标,然后求差。但是具有相同规模数组的下标分布没有什么规律可寻;
然后我注意到经过归并后的数组,其下标都是重复出现的,比如0-24子数组的下标0、24;25-49子数组的下标25 、49等,通过统计下标出现的次数,来确定哪两个子数组进行合并,比如下图中二叉树的第三层0、24、25、49、50、74、75、99都是第二次出现,这样就可以使对应的子数组合并。但是存在的问题是统计下标出现的次数,就要对下标数组进行遍历查找,这样无形中又增加了开销,即便使用二分法,也是相对繁琐的,感觉不是个好的解决办法;
最后我使用的方法是用标记数组刚记录下来的子数组的下标low同记录的上一个子数组的high作比较,只要是low小于high,那么刚记录的下标low、high就是要合并的数组的两端,比如 下标数组arr={0,12,13,24,0,24,25,37,38,49,25,49......} ,arr[5] = 0 < arr[4] = 24,那么要合并的就是从 arr[5] 到 arr[6] 的子数组。
代码的不足:
1. 没有实现第三个改进
2. 代码中下标数组的大小,我是按照测试数据给的,最好是可以随着排序数据的规模调节大小。
3. 因为小范围的排序是使用的插入排序,所以适用于基本有序的数据,我测试用的数据是在做希尔排序的习题时写的一个完全逆序的一个排列数组,一个可能是数组太小,一个就是数组完全逆序,所以测试结果还不如普通的归并排序。当然也可能是代码写的有问题。
最后,受限于水平,如果有不对的地方,欢迎指正,交流学习,共同进步。
运行结果