合並排序法的概念
合並排序法是最典型的分治(Divide and Conquer)演算法,將一整個序列分割成一個個元素,再兩兩一組依照元素大小填入至新的空間中來合並成新的,並且已經排序好的序列。
合並排序法的過程
假設現在有個陣列資料,內容如下:
索引 0 1 2 3 4 5 6 7 8 9 數值 69 81 30 38 9 2 47 61 32 79
要如何將它遞增排列呢?
首先將陣列對半切成:
索引 0 1 2 3 4 | 5 6 7 8 9 數值 69 81 30 38 9 | 2 47 61 32 79
再對半切成:
索引 0 1 | 2 3 4 || 5 6 | 7 8 9 數值 69 81 | 30 38 9 || 2 47 | 61 32 79
再對半切成:
索引 0 | 1 || 2 | 3 4 ||| 5 | 6 || 7 | 8 9 數值 69 | 81 || 30 | 38 9 ||| 2 | 47 || 61 | 32 79
再對半切成:
索引 0 || 1 ||| 2 || 3 | 4 |||| 5 || 6 ||| 7 || 8 | 9 數值 69 || 81 ||| 30 || 38 | 9 |||| 2 || 47 ||| 61 || 32 | 79
已經不能再切了,即可開始合並,一邊合並一邊把元素照順序排好:
索引 0 | 1 || 2 | 3 4 ||| 5 | 6 || 7 | 8 9 數值 69 | 81 || 30 | 9 38 ||| 2 | 47 || 61 | 32 79 →───← →───←
繼續合併:
索引 0 1 | 2 3 4 || 5 6 | 7 8 9 數值 69 81 | 9 30 38 || 2 47 | 32 61 79 →────← →────────← →───← →────────←
繼續合併:
索引 0 1 2 3 4 | 5 6 7 8 9 數值 9 30 38 69 81 | 2 32 47 61 79 →─────────────────← →─────────────────←
技術合併:
索引 0 1 2 3 4 5 6 7 8 9 數值 2 9 30 32 38 47 61 69 79 81 →────────────────────────────────────────←
合並完成,也排序完成了!
以上過程看起來十分直覺。
合並排序法的程式實作
/** * 合並排序法(遞增),使用遞回。此為用來遞回呼叫的函數。 */ public static void mergeSortRecursively(final int[] array, final int[] buffer, final int start, final int end) { final int length = end - start + 1; if (length < 2) { return; } final int middle = length / 2 + start; int ls = start; final int le = middle - 1; int rs = middle; final int re = end; mergeSortRecursively(array, buffer, ls, le); mergeSortRecursively(array, buffer, rs, re); int p = start; while (ls <= le && rs <= re) { buffer[p++] = array[ls] < array[rs] ? array[ls++] : array[rs++]; } while (ls <= le) { buffer[p++] = array[ls++]; } while (rs <= re) { buffer[p++] = array[rs++]; } System.arraycopy(buffer, start, array, start, length); } /** * 合並排序法(遞增),使用遞回。 */ public static void mergeSort(final int[] array) { final int[] buffer = new int[array.length]; mergeSortRecursively(array, buffer, 0, array.length - 1); } /** * 合並排序法(遞減),使用遞回。此為用來遞回呼叫的函數。 */ public static void mergeSortDescRecursively(final int[] array, final int[] buffer, final int start, final int end) { final int length = end - start + 1; if (length < 2) { return; } final int middle = length / 2 + start; int ls = start; final int le = middle - 1; int rs = middle; final int re = end; mergeSortDescRecursively(array, buffer, ls, le); mergeSortDescRecursively(array, buffer, rs, re); int p = start; while (ls <= le && rs <= re) { buffer[p++] = array[ls] > array[rs] ? array[ls++] : array[rs++]; } while (ls <= le) { buffer[p++] = array[ls++]; } while (rs <= re) { buffer[p++] = array[rs++]; } System.arraycopy(buffer, start, array, start, length); } /** * 合並排序法(遞減),使用遞回。 */ public static void mergeSortDesc(final int[] array) { final int[] buffer = new int[array.length]; mergeSortDescRecursively(array, buffer, 0, array.length - 1); }
在實作程式的時候,應該要避免使用遞回(Recursive)的結構,因為遞回需要不斷地重新建構函數的堆疊空間,硬體資源的負擔會比較大,且若是遞回層數過多還會導致堆疊溢出(Stack Overflow)。所以比較好的做法還是在一個函數內以回圈迭代的方式來完成。
為了簡化遞回轉迭代結構時的問題,在這里不采取從中間開始分割的方式,而是直接跳過分割的動作,從合並開始進行,在觀察合並排序算法的分割過程后,其實不難發現完整的子陣列的長度,合並過后的長度都會再乘以2,並且即便是未達2的冪長度的陣列,也能進行合並的動作,意思就是說,分割的動作其實並非必要,可以直接從前端開始直接合並2的冪個元素,先從1(20)個元素中兩兩開始合並,再從2(21)個元素兩兩合並,再從4(22)個元素兩兩開始合並,再從8(23)個元素兩兩開始合並,一次類推,因此可以寫出如下迭代版的合並排序算法。
/** * 合並排序法(遞增),使用回圈來迭代。 */ public static void mergeSort(final int[] array) { final int length = array.length; final int lengthDec = length - 1; final int[] buffer = new int[length]; for (int width = 1; width < length; width *= 2) { final int doubleWidth = width * 2; final int e = length - width; for (int start = 0; start < e; start += doubleWidth) { int end = start + doubleWidth - 1; if (end >= length) { end = lengthDec; } final int middle = start + width; int ls = start; final int le = middle - 1; int rs = middle; final int re = end; int p = start; while (ls <= rs && rs <= re) { if (array[ls] < array[rs]) { buffer[p++] = array[ls]; } else { buffer[p++] = array[rs++]; } } while (ls <= le) { buffer[p++] = array[ls++]; } while (rs <= re) { buffer[p++] = array[rs++]; } System.arraycopy(buffer, start, array, start, end - start + 1); } } } /** * 合並排序法(遞減),使用回圈來迭代。 */ public static void mergeSortDesc(final int[] array) { final int length = array.length; final int lengthDec = length - 1; final int[] buffer = new int[length]; for (int width = 1; width < length; width *= 2) { final int doubleWidth = width * 2; final int e = length - width; for (int start = 0; start < e; start += doubleWidth) { int end = start + doubleWidth - 1; if (end >= length) { end = lengthDec; } final int middle = start + width; int ls = start; final int le = middle - 1; int rs = middle; final int re = end; int p = start; while (ls <= rs && rs <= re) { if (array[ls] > array[rs]) { buffer[p++] = array[ls]; } else { buffer[p++] = array[rs++]; } } while (ls <= le) { buffer[p++] = array[ls++]; } while (rs <= re) { buffer[p++] = array[rs++]; } System.arraycopy(buffer, start, array, start, end - start + 1); } } }
實際上使用合並排序法時,常會去檢查子序列的大小是否過長(長度大於7~15),如果子序列長度不長,會使用適合拿來排序少量資料的插入排序等演算法來完成排序。
合並排序法的復雜度
項目 | 值 | 備注 |
---|---|---|
最差時間復雜度 | O(nlogn)O(nlogn) | |
最佳時間復雜度 | O(nlogn)O(nlogn) | |
平均時間復雜度 | O(nlogn)O(nlogn) | |
額外最差空間復雜度 | O(n)O(n) | |
是否穩定 | 是 |