在介绍 归并排序(merge sort) 算法之前, 我们先学习一下 分治法 .
算法的设计技术有很多, 插入排序 使用了 增量 方法:
在排序子数组 a[0..i-1]
后, 将单个元素 a[i]
插入到子数组的适当位置, 产生排序好的子数组 a[0..i]
.
这里我们学习另一种算法设计方法: 分治法.
1 分治法(The divide-and-conquer approach)
很多算法在 结构上是递归的: 为了解决一个给定的的问题, 算法 多次递归地调用自身 以解决紧密相关的若干 子问题.
这些算法典型地遵循 分治法(Divide-and-Conquer) 的思想:
将原问题分解为 多个规模较小但类似原问题的 子问题, 递归地求解这些子问题, 然后再合并这些子问题的解 以求得原问题的解.
分治模式(divide-and-conquer paradigm) 在每层递归时都有 3 个步骤:
分解(Divide) 原问题为若干子问题. 这些子问题都是原问题的规模较小的实例.
解决(Conquer) 这些子问题, 递归地求解各个子问题. 当子问题规模足够小时, 可以直接求解(不用递归).
合并(Combine) 这些子问题的解 成 原问题的解.
2 归并排序(merge sort)
归并排序(merge sort) 算法完全遵循 分治模式:
分解(divide): 分解待排序的 n
个元素的序列 成 各具 n/2
个元素的 两个子序列.
解决(conquer): 使用归并排序递归地排序 两个子序列.
合并(combine): 合并 两个已排好序的子序列 以 产生已排好序的答案.
归并排序的关键在于 合并.
这个合并过程就好比:
桌上有两堆牌, 每堆都排好序(最小的在最上面), 现在要把两堆合成一堆. 我们可以比较两堆的最上面的一张牌, 把小的一张取出放在手上; 重复这个动作最终会有一个堆的牌会先取完, 这时把另外一个堆的牌全部依次放在手上. 这样手上的牌就是 两堆牌合并后的 有序的牌.
我们通过一个辅助过程 merge(a[], begin, mid, end)
来完成合并. 其中 a[]
是一个数组, begin
是数组中需要合并的起始下标, end
是需要合并的终点下标, mid
是 begin
和 end
的中间下标. begin
<= mid
< end
.
这个过程的示意图:
从a[begin..end]
中分出 left[begin..mid]
和 right[mid+1..end]
两个数组:
循环比较 left[]
和 right[]
的元素(黄色水彩标出的元素), 把小的元素放入 a[]
并把响应的下标往后移(l
或r
), 图中绿色水彩标出的元素表示已经排好序:
归并排序代码实现:
public class MergeSort2 {
public void mergeSort(int[] a, int begin, int end) {
if (begin < end) {
int mid = (begin + end) / 2;
mergeSort(a, begin, mid);
mergeSort(a, mid + 1, end);
merge(a, begin, mid, end);
}
}
private void merge(int[] a, int begin, int mid, int end) {
int[] left = new int[mid - begin + 1];
int[] right = new int[end-mid];
for (int i = 0; i < left.length; i++) {
left[i] = a[begin + i];
}
for (int i = 0; i < right.length; i++) {
right[i] = a[mid + 1 + i];
}
int l = 0, r = 0;
int i = begin;
while (i <= end) {
if (l >= left.length || r >= right.length) {
break;
}
if (left[l] <= right[r]) {
a[i++] = left[l++];
} else {
a[i++] = right[r++];
}
}
if (l >= left.length) { // 左边序列元素读完, 直接把右边的序列依次放入a中
while (r < right.length) {
a[i++] = right[r++];
}
} else { // 右边序列元素读完, 直接把左边的序列依次放入a中
while (l < left.length) {
a[i++] = left[l++];
}
}
}
public static void main(String[] args) {
int[] a = {4, 5, 1, 2, 7, 11, 3};
MergeSort2 mergeSort = new MergeSort2();
mergeSort.mergeSort(a, 0, a.length - 1);
System.out.println(Arrays.toString(a));
}
}
merge
方法看似很长, 其实思想很简单.
由以上 分析/代码 可知, 归并排序是 稳定的排序算法. 不会因a[]
的顺序 对时间复杂度产生什么影响.
因为merge
操作需要O(n)
的时间, 递归产生的递归树高 lgn
, 树的每层调用代价都是 n
, 所以归并排序是时间复杂度是 O(nlgn)
.