最近在看謝路雲翻譯的《算法》,這本書真是相當不錯,值得好好研讀。閱讀之餘也試着做每節後面的習題。這幾天看到那幾個基礎的排序,歸併排序那一章節後面有這樣的一到習題:
改進:實現前面所述的對歸併排序的三項改進,加快小數組的排序速度,檢測數組是否已經有序以及通過在地櫃中交換參數來避免數組複製。
書中提到的三項對歸併排序的改進如下: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. 因爲小範圍的排序是使用的插入排序,所以適用於基本有序的數據,我測試用的數據是在做希爾排序的習題時寫的一個完全逆序的一個排列數組,一個可能是數組太小,一個就是數組完全逆序,所以測試結果還不如普通的歸併排序。當然也可能是代碼寫的有問題。
最後,受限於水平,如果有不對的地方,歡迎指正,交流學習,共同進步。
運行結果