合并排序算法
合并排序法的概念
合并排序法是最典型的分治(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) | |
是否稳定 | 是 |