演算法(4)歸併排序 java
在介紹歸併排序之前,先簡單的說一下O(NlogN)和O(N2)之間的比較,通過下面的圖片可以明顯的看出來,前者的優勢是很明顯的,並且隨著N的增大,優勢會越來越明顯,優化之後的程式碼可能意味著笨的演算法一輩子都算不出來結果,而優化之後的演算法,一瞬間就算出來了(細思極恐,這不就是現實生活嗎...)
歸併排序:歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路
為什麼說歸併排序就是一個O(NlogN)的演算法呢?請看下圖
一個有N個元素的陣列,假設N=8,那麼採用分治法,通過上圖可以看出來,進行3次二分法就能將陣列拆分為單個元素,然後逐層進行歸併操作,3就是通過log28=3得出來的 通過這種拆分方法,把問題降低到通過O(N)的時間複雜度就能得到一個排好序的子序列,因此歸併排序是一個O(NlogN)的演算法,其在效能上要比O(N2)要好,並且是一個很穩定的演算法.
當然任何事情都是有兩面性,歸併演算法的缺陷是需要藉助於比較大的記憶體,但通過空間換取時間是比較值得的
可以使用遞迴思想或者迴圈來實現歸併排序,本次程式碼中使用遞迴思想來實現歸併排序
// [l,r] public static void sort(int[] arr, int l, int r) { if (l >= r) return; int mid = (r + l) / 2; sort(arr, l, mid); sort(arr, mid + 1, r); merge(arr, l, r, mid); } // [l,mid] [mid+1,r]是已經排好序的 直接進行merge操作 private static void merge(int[] arr, int l, int r, int mid) { int[] aux = new int[r - l + 1];// 大小從l 到r for (int i = l; i <= r; i++) {// 將arr中l到r的元素複製到aux中 aux[i - l] = arr[i]; } int i = l;// 在arr中的 左邊有序的下標 int j = mid + 1;//在arr中的 右邊有序的下標 // 進行歸併操作 for (int k = l; k <= r;) { if (i > mid) {// 說明左邊已經比完了 直接將右邊j下標對應的數字給arr arr[k++] = aux[j++ - l]; } else if (j > r) {// 說明右邊已經比完了,直接將左邊i下標對應的數字給arr arr[k++] = aux[i++ - l]; } else {// 否則比較值 直接用三目運算 arr[k++] = aux[i - l] < aux[j - l] ? aux[i++ - l] : aux[j++ - l]; } } }
然而,上面的程式碼在某些情況下,其效能是不如插入排序的,因此歸併排序是有一些優化的點的.來分析程式碼
public static void sort(int[] arr, int l, int r) {
if (l >= r)
return;
int mid = (r + l) / 2;
sort(arr, l, mid);
sort(arr, mid + 1, r);
merge(arr, l, r, mid);
}
①在上面的程式碼中,將[l,mid]和[mid+1,r]兩個區間進行歸併排序,緊接著無論上面的兩個區間是否已經達成了有序不做判斷,直接進行merge操作,這樣在近乎有序的情況下,其效能會有影響.
②如果陣列近乎有序,那麼在元素個數小於一個常數的時候,利用插入排序效能反而更好,至於用哪個常數,在不同的應用場景,可以進行試驗.
經過兩次優化之後的程式碼:
// [l,r]
public static void sort(int[] arr, int l, int r) {
if (r - l + 1 <= 7) {// 優化②當需要進行歸併排序的個數小於7的時候直接使用插入排序演算法
// 插入排序演算法
for (int i = l; i <= r; i++) {
int t = arr[i];
int j = i;
for (; j > 0 && arr[j - 1] > t; j--) {
arr[j] = arr[j - 1];
}
if (j != i) {
arr[j] = t;
}
}
return;
}
int mid = (r + l) / 2;
sort(arr, l, mid);
sort(arr, mid + 1, r);
if (arr[mid] > arr[mid + 1]) {// 優化① 通過上面的步驟可以保證左區間和右區間都是有序的序列了,因此, //只有當左區間的最後一個元素大於右區間第一個元素時,才進行merge操作
merge(arr, l, r, mid);
}
}
// [l,mid] [mid+1,r]是已經排好序的 直接進行merge操作
private static void merge(int[] arr, int l, int r, int mid) {
int[] aux = new int[r - l + 1];// 大小從l 到r
for (int i = l; i <= r; i++) {// 將arr中l到r的元素複製到aux中
aux[i - l] = arr[i];
}
int i = l;// 在arr中的 左邊有序的下標
int j = mid + 1;// 在arr中的 右邊有序的下標
// 進行歸併操作
for (int k = l; k <= r;) {
if (i > mid) {// 說明左邊已經比完了 直接將右邊j下標對應的數字給arr
arr[k++] = aux[j++ - l];
} else if (j > r) {// 說明右邊已經比完了,直接將左邊i下標對應的數字給arr
arr[k++] = aux[i++ - l];
} else {// 否則比較值 直接用三目運算
arr[k++] = aux[i - l] < aux[j - l] ? aux[i++ - l] : aux[j++ - l];
}
}
}
通過上面的優化,這個歸併排序就已經算是達到了比較好的效能了.
-----------------------------------------------------------------分水嶺-----------------------------------
再附上一個不實用遞迴,使用迭代來完成的歸併排序,這個比較好理解
/**
* 通過自底向上的迭代來完成歸併排序
* @param arr
*/
public static void sortBU(int[] arr) {
for(int size = 1 ; size <= arr.length - 1 ; size += size) {//控制每次進行merge操作的元素的個數 2倍 關係 1->2->4
for(int i = 0 ; i + size <= arr.length - 1 ; i +=(size * 2)) {//控制每次merge操作的兩個區間[i,i+size - 1],[i + size , i+size * 2 - 1]
merge(arr, i, Math.min(i + size * 2 - 1,arr.length - 1), i + size - 1);//為了避免i+size * 2 - 1 出現越界 使用Math.min 來取出 當前的right 和陣列邊界小的那一個
}
}
}