在前三篇博客中,笔者分别讲述了冒泡排序、选择排序以及插入排序,这三种排序算法是比较基本算法,原理也好,实现也罢,难度都不是很大。笔者在这篇博客中,打算聊聊合并排序(Merge-Sort)。
《算法导论》中提到,合并排序是分治(Divide-and-Conquer)思想的第一个应用。分治思想中,对于一个给定的问题,将该问题分解为多个子问题,每个子问题又可以向下继续分解,直到分解出的子问题十分容易地就能够求解出来。然后将每个子问题的解合并起来,就是原始问题的最终解。合并排序的流程如下:
- 将一个数组分成左右两个元素个数差不多相等的左右两个数组,对应数组区间分别为[start, mid]以及[mid + 1, end]。
- 若当前数组只含有一个元素,即end == start,则表示返回上一步(递归中的归来),对两个数组进行合并操作。
- 合并的时候,需要将两个区间内的数组元素取出来分别存放于两个数组之内,然后比较两个数组的头部,头元素较小的往原数组里赋值,并且赋值数组和被赋值数组的指针分别向后移动一位(具体参见代码)。若移动过程中,有一个数组移动到末尾,则将另外一个数组的剩余部分直接复制到原始数组对应位置的后面。
合并排序的时间复杂度大致为O(n*log(n)),通常情况下来说,其也是一种稳定的排序算法。
假设有数组如下:
[7, 3, 14, 10, 9, 14, 7, 14]
当二分进行到最下面一层的时候,有8个子数组分别为:
[7], [3], [14], [10], [9], [14], [7], [14]
然后依次是对[7]和[3]的合并操作,合并结果为[3, 7],然后是对[14]和[10]的合并,结果为[10, 14],然后是对[3, 7]和[10, 14]的合并,合并结果为[3, 7, 10, 14],以此类推。最终得到的排序结果为:
[3, 7, 7, 9, 10, 14, 14, 14]
合并排序算法的示例代码如下:
import com.sun.istack.internal.NotNull;
import java.util.Arrays;
import java.util.Random;
/**
* A demo of {@code MergeSort}.
*
* @author Mr.K
*/
public class MergeSort {
public static void main(String[] args) {
int N = 20;
int[] numbers = new int[N];
Random random = new Random();
for (int i = 0; i < N; i++) {
numbers[i] = random.nextInt(2 * N);
}
System.out.println("待排序数组: " + Arrays.toString(numbers) + "\n");
mergeSort(numbers, 0, numbers.length - 1);
System.out.println("\n已排序数组: " + Arrays.toString(numbers));
}
/**
* Accepts an array, a number representing start index(inclusive) and a number
* representing end index(inclusive) and sorts the array by {@code MergeSort}.
* <ul>
* <li>If parameter <em>start</em> is less than <em>end</em>, which means
* there are at least two numbers to be sorted. And if so, finds the middle
* index and sorts the index from <em>start</em>(inclusive) to <em>mid</em>
* (inclusive), and from <em>mid + 1</em>(inclusive) to <em>end</em>(inclusive)
* by invoking {@code mergeSort} itself, respectively. As you can see, this
* is what we called <em>Recursion</em>.</li>
* <li>After two recursions, an operation called <em>merge</em> should be
* invoked to merge two sub-array, ensuring numbers from <em>start</em>
* (inclusive) to <em>end</em>(inclusive) are in an ascending order.</li>
* </ul>
* Be aware that when invoking this method, parameters <em>start</em> and <em>end
* </em> should be the first element and the last element of the specified array,
* respectively. In other words, if you'd like to sort all elements in an array,
* you should invoke this method like
* <blockquote>
* mergeSort(numbers, 0, numbers.length - 1);
* </blockquote>
* where <em>numbers</em> is an array of integer number.<br><br>
* <p>
* When there are enough numbers to be sorted via {@code MergeSort}, the cost of time
* of {@code MergeSort} is O(n * lgn), which is much better than O(n^2), the cost of
* time of {@link org.vimist.pro.sort.InsertionSort#insertionSort(int[])},
* {@link org.vimist.pro.sort.BubbleSort#bubbleSort(int[])},
* {@link org.vimist.pro.sort.SelectionSort#selectionSort(int[])}.<br><br>
* <p>
* To some conclusions, {@code MergeSort} is a form of thinking <em>Divide-and-Conquer
* </em>, which divides question into sub-questions and sub-question are divided into
* sub-sub-questions until questions are easy to be conquered. After conquering, merge
* the results and the whole questions will be resolved. Usually, when implementing
* <em>Divide-and-Conquer</em>, <em>Recursion</em> is used, which may be an obstacle to
* make full use of this thinking.
* <p>Final words, {@code MergeSort} is stable cause when merging, the condition is
* <blockquote>
* if (L[i] <= R[j])<br>
* </blockquote>
* As you can see, array <em>L</em> located at the left of array <em>R</em>. Thus, it's
* stable.
* </p>
*
* @param numbers specified array to be sorted
* @param start index of the beginning of the range of sort
* @param end index of the end of the range of sort
*/
public static void mergeSort(@NotNull int[] numbers, @NotNull int start, @NotNull int end) {
if (start < end) {
int mid = start + (end - start) / 2;
mergeSort(numbers, start, mid);
mergeSort(numbers, mid + 1, end);
merge(numbers, start, mid, end);
} else {
return;
}
}
/**
* Accepts an array and merges two sub-arrays, one of which starts from <em>start</em>
* (inclusive) to <em>mid</em>(inclusive) and the other one starts from <em>mid + 1</em>
* (inclusive) to <em>end</em>(inclusive).
* <ul>
* <li>This first step in this process is to assign numbers from index <em>start</em>
* (inclusive) to index <em>mid</em>(inclusive) and numbers from index <em>mid + 1</em>
* (inclusive) to index <em>end</em>(inclusive) to two arrays, named <em>L</em> and
* <em>R</em>, respectively.</li>
* <li>And then, compares the head of both sub-arrays, finds the minimum number from
* <em>L</em> and <em>R</em> and assigns that number to the <em>start</em> position
* and moves two cursor, one of which is the assigned array, the other one is the original
* array, to the next position.</li>
* <li>If one of these two arrays goes to the end, the remaining of the other one should
* be placed to the next position of original array directly.</li>
* </ul>
* At last, the cost of time of {@code Merge} is O(n) if and only if there are <em>n</em>
* numbers to be merged.
*
* @param number specified array to be merged
* @param start start index of the range to merge
* @param mid middle index
* @param end end index of the range to merge
*/
public static void merge(@NotNull int[] number, @NotNull int start, @NotNull int mid, @NotNull int end) {
/**
* Assign numbers in the range of (start, mid) and the range of (mid + 1, end)
* to two sub-arrays.
*/
int[] L = new int[mid - start + 1], R = new int[end - mid];
for (int i = 0; i < L.length; i++) {
L[i] = number[start + i];
}
for (int i = 0; i < R.length; i++) {
R[i] = number[mid + 1 + i];
}
System.out.println("待合并数组: " + Arrays.toString(L) + ", " + Arrays.toString(R));
/**
* Compares and re-assigns to ensure that the numbers in the range of (start, end)
* in ascending order.
*/
int i = 0, j = 0, k = start;
for (; i < L.length && j < R.length && k < end; k++) {
if (i < L.length && j < R.length) {
/**
* Making {@code MergeSort} stable
*/
if (L[i] <= R[j]) {
number[k] = L[i++];
} else {
number[k] = R[j++];
}
}
}
/**
* If one of both sub-arrays goes to the end, then assigning without comparing.
*/
while (i < L.length) {
number[k++] = L[i++];
}
while (j < R.length) {
number[k++] = R[j++];
}
}
}
其运行结果为:
待排序数组: [10, 9, 26, 16, 6, 34, 21, 30, 21, 5, 33, 13, 2, 1, 2, 0, 18, 11, 25, 11]
待合并数组: [10], [9]
待合并数组: [9, 10], [26]
待合并数组: [16], [6]
待合并数组: [9, 10, 26], [6, 16]
待合并数组: [34], [21]
待合并数组: [21, 34], [30]
待合并数组: [21], [5]
待合并数组: [21, 30, 34], [5, 21]
待合并数组: [6, 9, 10, 16, 26], [5, 21, 21, 30, 34]
待合并数组: [33], [13]
待合并数组: [13, 33], [2]
待合并数组: [1], [2]
待合并数组: [2, 13, 33], [1, 2]
待合并数组: [0], [18]
待合并数组: [0, 18], [11]
待合并数组: [25], [11]
待合并数组: [0, 11, 18], [11, 25]
待合并数组: [1, 2, 2, 13, 33], [0, 11, 11, 18, 25]
待合并数组: [5, 6, 9, 10, 16, 21, 21, 26, 30, 34], [0, 1, 2, 2, 11, 11, 13, 18, 25, 33]
已排序数组: [0, 1, 2, 2, 5, 6, 9, 10, 11, 11, 13, 16, 18, 21, 21, 25, 26, 30, 33, 34]
后来,笔者在《算法导论》上后面的例题中看到了一个要求,在合并排序中使用插入排序,题目的大意是:
在合并排序算法中,当子问题足够下时,考虑使用插入排序,对n/k个长度为k的子列表进行排序,然后再用标准的合并机制将它们合并在一起。此处的k是一个特定的值。
要求中有求解k的最大渐近值是什么,由于笔者才疏学浅,这里就没有论证,仅仅只是将插入排序应用到合并排序中去。在合并排序中,当end - start > k
时,子数组的长度大于k,继续分解。当end - start <= k
时,对子数组使用插入排序算法。排序后进行两个数组的合并操作并返回至上一次递归处继续下一步操作。
import com.sun.istack.internal.NotNull;
import java.util.Arrays;
import java.util.Random;
/**
* A demo of {@code MergeSort} combining with {@code InsertionSort}.
*
* @author Mr.k
*/
public class CombineMergeAndInsertion {
public static void main(String[] args) {
int N = 40;
int[] numbers = new int[N];
Random random = new Random();
for (int i = 0; i < N; i++) {
numbers[i] = random.nextInt(2 * N);
}
System.out.println("待排序数组: " + Arrays.toString(numbers) + "\n");
mergeSort(numbers, 0, numbers.length - 1, 4);
System.out.println("\n已排序数组: " + Arrays.toString(numbers));
}
/**
* Accepts an array and merges two sub-arrays, one of which starts from <em>start</em>
* (inclusive) to <em>mid</em>(inclusive), and the other one starts from <em>mid + 1</em>
* (inclusive) to <em>end</em>(inclusive).
* <ul>
* <li>The first step of the process of this method is to check whether <em>end - start
* </em> is greater than <em>k</em>. If so, then invokes this method itself from the
* range of [start, mid] and the range of [mid + 1, end] where
* <blockquote>
* mid = start + (end - start) / 2
* </blockquote>
* And then merge these two array where the total range is from <em>start</em> to
* <em>end</em>.</li>
* <li>On the other hand, when <em>end - start</em> is less than or equals to <em>k</em>,
* then {@link org.vimist.pro.sort.InsertionSort#insertionSort(int[])} is used to sort
* the sub-array in the range of [start, end].</li>
* </ul>
* This is a combination of {@code MergeSort} and {@code InsertionSort}. In some bad cases,
* the cost of time of this combination is O(nk + n * lg(n / k)).
*
* @param arr specified array
* @param start start index
* @param end end index
* @param k maximum number to apply {@code InsertionSort}
*/
public static void mergeSort(@NotNull int[] arr, @NotNull int start, @NotNull int end, @NotNull int k) {
if (end - start >= k) {
int mid = start + (end - start) / 2;
mergeSort(arr, start, mid, k);
mergeSort(arr, mid + 1, end, k);
merge(arr, start, mid, end);
} else {
int[] array = new int[end - start + 1];
System.arraycopy(arr, start, array, 0, end - start + 1);
System.out.println("待排序部分数组: " + Arrays.toString(array));
for (int i = start + 1; i <= end; i++) {
int key = arr[i];
int j = i - 1;
System.out.println("插入排序, 待排序数字: " + key);
while (j >= start && arr[j] > key) {
arr[j + 1] = arr[j--];
}
arr[j + 1] = key;
}
System.arraycopy(arr, start, array, 0, end - start + 1);
System.out.println("已排序部分数组: " + Arrays.toString(array));
}
}
/**
* Accepts an array and merges two sub-arrays, one of which starts from <em>start</em>
* (inclusive) to <em>mid</em>(inclusive) and the other one starts from <em>mid + 1</em>
* (inclusive) to <em>end</em>(inclusive).
* <ul>
* <li>This first step in this process is to assign numbers from index <em>start</em>
* (inclusive) to index <em>mid</em>(inclusive) and numbers from index <em>mid + 1</em>
* (inclusive) to index <em>end</em>(inclusive) to two arrays, named <em>L</em> and
* <em>R</em>, respectively.</li>
* <li>And then, compares the head of both sub-arrays, finds the minimum number from
* <em>L</em> and <em>R</em> and assigns that number to the <em>start</em> position
* and moves two cursor, one of which is the assigned array, the other one is the original
* array, to the next position.</li>
* <li>If one of these two arrays goes to the end, the remaining of the other one should
* be placed to the next position of original array directly.</li>
* </ul>
* At last, the cost of time of {@code Merge} is O(n) if and only if there are <em>n</em>
* numbers to be merged.
*
* @param number specified array to be merged
* @param start start index of the range to merge
* @param mid middle index
* @param end end index of the range to merge
*/
public static void merge(@NotNull int[] number, @NotNull int start, @NotNull int mid, @NotNull int end) {
/**
* Assign numbers in the range of (start, mid) and the range of (mid + 1, end)
* to two sub-arrays.
*/
int[] L = new int[mid - start + 1], R = new int[end - mid];
for (int i = 0; i < L.length; i++) {
L[i] = number[start + i];
}
for (int i = 0; i < R.length; i++) {
R[i] = number[mid + 1 + i];
}
System.out.println("待合并数组: " + Arrays.toString(L) + ", " + Arrays.toString(R));
/**
* Compares and re-assigns to ensure that the numbers in the range of (start, end)
* in ascending order.
*/
int i = 0, j = 0, k = start;
for (; i < L.length && j < R.length && k < end; k++) {
if (i < L.length && j < R.length) {
/**
* Making {@code MergeSort} stable
*/
if (L[i] <= R[j]) {
number[k] = L[i++];
} else {
number[k] = R[j++];
}
}
}
/**
* If one of both sub-arrays goes to the end, then assigning without comparing.
*/
while (i < L.length) {
number[k++] = L[i++];
}
while (j < R.length) {
number[k++] = R[j++];
}
}
}
运行结果如下:
待排序数组: [37, 24, 40, 74, 72, 51, 35, 60, 0, 5, 70, 58, 12, 79, 26, 53, 11, 8, 67, 71, 54, 49, 38, 76, 26, 70, 69, 62, 48, 48, 41, 56, 27, 71, 43, 44, 62, 37, 79, 0]
待排序部分数组: [37, 24, 40]
插入排序, 待排序数字: 24
插入排序, 待排序数字: 40
已排序部分数组: [24, 37, 40]
待排序部分数组: [74, 72]
插入排序, 待排序数字: 72
已排序部分数组: [72, 74]
待合并数组: [24, 37, 40], [72, 74]
待排序部分数组: [51, 35, 60]
插入排序, 待排序数字: 35
插入排序, 待排序数字: 60
已排序部分数组: [35, 51, 60]
待排序部分数组: [0, 5]
插入排序, 待排序数字: 5
已排序部分数组: [0, 5]
待合并数组: [35, 51, 60], [0, 5]
待合并数组: [24, 37, 40, 72, 74], [0, 5, 35, 51, 60]
待排序部分数组: [70, 58, 12]
插入排序, 待排序数字: 58
插入排序, 待排序数字: 12
已排序部分数组: [12, 58, 70]
待排序部分数组: [79, 26]
插入排序, 待排序数字: 26
已排序部分数组: [26, 79]
待合并数组: [12, 58, 70], [26, 79]
待排序部分数组: [53, 11, 8]
插入排序, 待排序数字: 11
插入排序, 待排序数字: 8
已排序部分数组: [8, 11, 53]
待排序部分数组: [67, 71]
插入排序, 待排序数字: 71
已排序部分数组: [67, 71]
待合并数组: [8, 11, 53], [67, 71]
待合并数组: [12, 26, 58, 70, 79], [8, 11, 53, 67, 71]
待合并数组: [0, 5, 24, 35, 37, 40, 51, 60, 72, 74], [8, 11, 12, 26, 53, 58, 67, 70, 71, 79]
待排序部分数组: [54, 49, 38]
插入排序, 待排序数字: 49
插入排序, 待排序数字: 38
已排序部分数组: [38, 49, 54]
待排序部分数组: [76, 26]
插入排序, 待排序数字: 26
已排序部分数组: [26, 76]
待合并数组: [38, 49, 54], [26, 76]
待排序部分数组: [70, 69, 62]
插入排序, 待排序数字: 69
插入排序, 待排序数字: 62
已排序部分数组: [62, 69, 70]
待排序部分数组: [48, 48]
插入排序, 待排序数字: 48
已排序部分数组: [48, 48]
待合并数组: [62, 69, 70], [48, 48]
待合并数组: [26, 38, 49, 54, 76], [48, 48, 62, 69, 70]
待排序部分数组: [41, 56, 27]
插入排序, 待排序数字: 56
插入排序, 待排序数字: 27
已排序部分数组: [27, 41, 56]
待排序部分数组: [71, 43]
插入排序, 待排序数字: 43
已排序部分数组: [43, 71]
待合并数组: [27, 41, 56], [43, 71]
待排序部分数组: [44, 62, 37]
插入排序, 待排序数字: 62
插入排序, 待排序数字: 37
已排序部分数组: [37, 44, 62]
待排序部分数组: [79, 0]
插入排序, 待排序数字: 0
已排序部分数组: [0, 79]
待合并数组: [37, 44, 62], [0, 79]
待合并数组: [27, 41, 43, 56, 71], [0, 37, 44, 62, 79]
待合并数组: [26, 38, 48, 48, 49, 54, 62, 69, 70, 76], [0, 27, 37, 41, 43, 44, 56, 62, 71, 79]
待合并数组: [0, 5, 8, 11, 12, 24, 26, 35, 37, 40, 51, 53, 58, 60, 67, 70, 71, 72, 74, 79], [0, 26, 27, 37, 38, 41, 43, 44, 48, 48, 49, 54, 56, 62, 62, 69, 70, 71, 76, 79]
已排序数组: [0, 0, 5, 8, 11, 12, 24, 26, 26, 27, 35, 37, 37, 38, 40, 41, 43, 44, 48, 48, 49, 51, 53, 54, 56, 58, 60, 62, 62, 67, 69, 70, 70, 71, 71, 72, 74, 76, 79, 79]