簡單來說,歸併排序主要是將多個有序的序列合併爲一個有序的序列.
我們首先看下2個簡單的數 5 , 2 ,那麼直接將5和2比較交換位置就變成 2,5 。然後再加2個數 4,3 ,同理,比較交換變成 3,4 。
現在我們將2,5 和 3,4 這2個序列合併爲一個有序序列, 對於沒有什麼算法基礎的同學第一個想到可能是先找出最小的數,然後是第二小的數,第三小的數等等,最後依次將其放入一個新數組中即可完成。
但是今天我們來討論另一種方案 , 我們先將2個序列的第一個數2和3進行比較選出較小的2放進新數組,然後將第一個序列下標後移一位爲5繼續和3比較選出較小的3放進新數組,再將第二個序列下標後移一位爲4繼續和5比較選出較小的4放進新數組,最後將5放進新數組,這樣新數組的數據就爲 2,3,4,5 的有序序列。這就叫做 歸併排序。
現在我們來看下歸併排序的代碼:
public static void mergeSort(int arr[],int left,int right){
if(left>=right)return;
int mid=(right+left)/2;
mergeSort(arr,left,mid);
mergeSort(arr,mid+1,right);
merge(arr,left,mid,right);
}
private static void merge(int[] arr, int left, int mid, int right) {
int tmp[]=new int[right-left+1];
int leftIndex=left;
int rightIndex=mid+1;
int tmpIndex=0;
while (leftIndex<=mid && rightIndex<=right){
if(arr[leftIndex]<=arr[rightIndex]){
tmp[tmpIndex++]=arr[leftIndex++];
}else{
tmp[tmpIndex++]=arr[rightIndex++];
}
}
int start=leftIndex,end=mid;
if(rightIndex<=right){
start=rightIndex;
end=right;
}
while (start<=end){
tmp[tmpIndex++]=arr[start++];
}
for(leftIndex=0;leftIndex<=right-left;leftIndex++){
arr[left+leftIndex]=tmp[leftIndex];
}
}
mergeSort 方法中的 arr 爲排序的數組, left 和 right 分別爲arr數組 的左邊界下標和右邊界下標,這裏是採用分治的方式對數組遞歸,然後進行歸併排序的操作。也就是對數組不斷進行二分,直至左邊界大於等於右邊界不能二分爲止。
現在我們來看下數組 5, 3, 2, 6, 4, 1 的歸併執行過程,其中合併就是指的是執行merge方法:
5 3 2 6 4 1 left=0 right=5
第一次遞歸 5 3 2 left=0 right=2
第二次遞歸 5 3 left=0 right=1
第三次遞歸 5 left=0 right=0 left>=right,達到終止條件返回
第四次遞歸 3 left=1 right=1 left>=right,達到終止條件返回
第一次合併 3 5 2 6 4 1 left=0 mid=0 right=1
第五次遞歸 2 left=2 right=2 left>=right,達到終止條件返回
第二次合併 2 3 5 6 4 1 left=0 mid=1 right=2
第六次遞歸 6 4 1 left=3 right=5
第六次遞歸 6 4 left=3 right=4
第六次遞歸 6 left=3 right=3 left>=right,達到終止條件返回
第七次遞歸 4 left=4 right=4 left>=right,達到終止條件返回
第三次合併 2 3 5 4 6 1 left=3 mid=3 right=4
第八次遞歸 1 left=5 right=5 left>=right,達到終止條件返回
第四次合併 2 3 5 1 4 6 left=3 mid=4 right=5
第五次合併 1 2 3 4 5 6 left=0 mid=2 right=5
上面就是執行歸併排序的詳細過程了,標紅是歸併過程中發生的數據交換。
現在我們來詳細解析一下merge方法,以上面的最後一次歸併爲例,我們看下merge的執行過程:
首先會創建一個 tmp 數組,用於存放已經排序的數據。然後以 mid 爲中心分爲左右2個子數組,分別用 leftIndex和rightIndex 2個下標進行遍歷,選出較小的那個數,將其對應的下標加1,循環往復,直至遍歷到終點爲止。最後將子數組中剩餘的數據放進tmp數組中,將tmp中的數據複製進原數組 arr 中即可。
歸併優化:
針對merge方法,還有一種優化的方式:
private static void mergeGuard(int[] arr, int left, int mid, int right) {
int[] leftArr = new int[mid - left + 2];
int[] rightArr = new int[right - mid + 1];
for (int i = 0; i <= mid - left; i++) {
leftArr[i] = arr[left + i];
}
// 第一個數組添加哨兵(最大值)
leftArr[mid - left + 1] = Integer.MAX_VALUE;
for (int i = 0; i < right - mid; i++) {
rightArr[i] = arr[mid + 1 + i];
}
// 第二個數組添加哨兵(最大值)
rightArr[right-mid] = Integer.MAX_VALUE;
int i = 0;
int j = 0;
int k = left;
while (k <= right) {
// 當左邊數組到達哨兵值時,i不再增加,直到右邊數組讀取完剩餘值,同理右邊數組也一樣
if (leftArr[i] <= rightArr[j]) {
arr[k++] = leftArr[i++];
} else {
arr[k++] = rightArr[j++];
}
}
}
這個主要是將左右數組的邊界分別存儲整型的最大值,這樣在歸併的時候,就可以將左右數組中的所有數據都放進arr中,不需要在數據比較完成後,額外將邊界數據放入新數組中。
總結:
歸併排序,本質上就是採用分治的方式,將序列一點點拆分爲一個個數,最後進行合併。但是人的思維都是正向的,將一個個數合併爲一個有序序列時很容易理解,逆向將序列拆分再合併就比較難理解一點。本文先採用拆分的方式,帶大家理解歸併的核心,然後帶領大家一步步的去深入理解歸併排序。