简述
算法导论中,在第二章提及了归并排序,归并排序是分治思想的一个重要实现,只要提及分治算法,就不得不提及归并排序。
原理
归并排序有 2 个步骤:
- 将数据平均分成 2 个序列,递归,将 2 个部分继续分解。直到序列中只剩下一个元素。
- 分解成功后,递归返回,进行合并,使用一个临时数组,将 2 个序列的数据进行有序合并,通过遍历序列中的元素,依次将最小的元素插入到临时序列中。最后,将临时序列中的元素依次插入到原来的数组中。
下面有张图,简单说明了一组无序数组,最终变成有序数组的过程:
图中,黑色的线,表示分解。红色的线,表示合并,且合并的过程中,使用到了临时数组。
最终,无序数组,就变成了有序数组。
代码实现:
static public void mergeSort(int[] arr, int low, int high) {
if (low >= high) {
return;
}
int mid = low + (high - low) / 2;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
static public void merge(int[] arr, int low, int mid, int high) {
int i = low;
// mid 是分隔点
int j = mid + 1;
int[] tmp = new int[high - low + 1];
int tmpIndex = 0;
while (j <= high && i <= mid) {
if (arr[j] <= arr[i]) {
tmp[tmpIndex++] = arr[j++];
} else {
tmp[tmpIndex++] = arr[i++];
}
}
int start = I;
int end = mid;
// 由于最后是 i++, 所以,这里判断要加 1
if (i >= mid + 1) {
start = j;
end = high;
}
// 剩余元素, 使用 <= end, 包括 end 元素.
for (int k = start; k <= end; k++) {
tmp[tmpIndex++] = arr[k];
}
for (int k = 0; k < tmpIndex; k++) {
arr[low++] = tmp[k];
}
}
上面的代码中,步骤如下:
- 在 mergeSort 方法中,将序列平均分成 2 个,然后继续递归,知道元素数量是 1.
- 当 2 个递归序列都返回之后,我们使用 merge 函数,进行合并。
合并是 归并排序的重要部分。
合并过程:
- 我们必须知道原数组,low,mid ,high 数据。并创建一个长度为 hight - low + 1 的临时数组。
- 从 mid 进行分割,设计 2 个指针:i 和 j。i 表示 mid 左边的数据,j 表示 mid 右边的数据。
- i 和 j 分别自增进行比较,谁小,就把谁插入到临时数组中,同时,将该指针前进一步。
- 停止条件为 i <=mid,表示左边的序列比较完毕,j <= high,表示右边的序列遍历完毕。
- 遍历结束任一一边的序列,都应该停止,然后将另一边的序列剩余元素,顺序拷贝到临时数组。
- 最后,将临时数组从 low 下标开始,插入到原数组中,完成排序。
下面是一个图示:
如果我们现在有 2 个序列
{0,3,4,8,9} 和 {1,2,5,6},合并的过程如下:
1
2
这里,i 和 j 开始比较。i 小,那么,就需要把 i 的元素,插入到右边的临时数组中。然后 i 前进一步。
3
i 前进一步继续和 j 比较,此时 j 要小,那么就将 j 插入到临时数组中,同时,j 前进一步。
4
继续,j 又前进一步。
5
继续比较,此时 i 比 j 小,应该把 i 插入到临时数组中,i 前进一步。
6
继续比较,i 还是小,重复上一步。
7
此时 j 比 i 小,j 插入到临时数组中,j 前进一步。
8
此时,j 已经达到尽头,无法继续循环,应当停止,并且,将 i —— mid 的元素,追加到临时数组中。完成排序。
复杂度
计算归并排序需要用到递归式:
image.png
通过推演,我们知道:
最好时间复杂度:
最坏时间复杂度:
平均时间复杂度(期望运行时间):
归并排序是一个非常稳定的排序算法。
由于每次递归都需要拷贝一个临时数组,而这个临时数组的长度最大就是 n,所以:
空间复杂度:
归并排序是稳定的吗?
我们假设两个数组中,有 2 个相同的元素:
image.png
我们必须将左边的元素先拷贝,然后再拷贝右边的元素,保证他们的顺序是不变的。
因此,我们的代码比较应该是:
if(arr[i] <= arr[j] {
插入 i;
}
因此,归并排序是稳定的。
另外,归并排序不是原地排序,因为,归并排序每次合并的过程中,都会使用一个临时数组。