前言
上一篇文章,介绍过第一种基于分治策略的排序算法--快速排序。接下来我们来讨论另一种基于分治策略的排序算法,归并排序。归并排序也被认为是一种时间复杂度最优的算法,我们还是按照基本过程,代码,最坏时间复杂度,平均时间复杂度,性能分析和改进这几个方面来展开讲述,为什么归并排序被认为是最优的基于比较的排序算法(理论上)。
一、归并排序过程
归并排序的思想:
将待排序序列划分成若干个有序子序列;将两个或两个以上的有序子序列合并为一个有序序列。
我们大体上有两种解决方案:
- 朴素归并排序
就像上面图片所展示的那样,先分再治。上面的图片已经非常形象的阐明了归并排序的整体过程。
上面这种过程的代码如下所示:
public void Merge(int[] E, int s, int m, int t) {
int[] B = new int[E.length];
int k, p, j = 0;
for (k = s, p = s, j = m + 1; p <= m && j <= t; ++k) {
if (E[p] <= E[j]) {
B[k] = E[p++];
} else B[k] = E[j++];
}
if (p <= m) {
for (int i = p; i <= m; i++) {
B[k++] = E[i];
}
}
if (j <= t) {
for (int i = j; i <= t; i++) {
B[k++] = E[i];
}
}
//写回原数组
for (p = s; p <= t; p++) E[p] = B[p];
}
public void Msort(int[] E, int s, int t) {
if (s < t) {
int m = 0;
m = (s + t) / 2;
Msort(E, s, m);
Msort(E, m + 1, t);
this.Merge(E, s, m, t);
}
}
上面这种实现方式就是先不断划分子问题,然后解决最小规模问题,再进行合并。实际上,我们还可以通过第二种方式对这种基本的归并排序算法做一些改进。
2. 二路归并排序
其实我们还有另外一种解决方案:将n个待排序的数据直接划分成n个规模为1的子序列。然后进行二路归并排序。
二路归并排序直接将n个待排序的数据元素看成n个有序子序列,依次合并两个相邻有序子序列,直到只剩下一个有序子序列为止。
示例代码如下所示:
public void Merge2(int[] E, int[] B, int s, int m, int t) {
int k, p, j = 0;
for (k = s, p = s, j = m + 1; p <= m && j <= t; ++k) {
if (E[p] <= E[j]) {
B[k] = E[p++];
} else {
B[k] = E[j++];
}
}
if (p <= m) {
for (int i = p; i <= m; i++) {
B[k++] = E[i];
}
}
if (j <= t) {
for (int i = j; i <= t; i++) {
B[k++] = E[i];
}
}
//写回原数组
for (p = s; p <= t; p++) E[p] = B[p];
}
public void Msort_n_re(int[] E, int n){
int B[] = new int[E.length];
int len = 1;
while (len < n) {
int i;
for (i = 0; i < n + 1 - 2 * len; i = i + 2 * len) {
Merge2(E, B, i, i + len - 1, i + 2 * len - 1);
}
if (i + len - 1 < n) {
Merge2(E, B, i, i + len - 1, n - 1);
} else {
for (; i < n; i++) E[i] = B[i];
}
len *= 2;
}
}
很容易可以看到区别,我们将递归过程改为非递归过程。
二、归并排序的时间复杂度分析
归并排序特点:
- 有序子序列的数据元素的个数≤Merge算法的比较次数
- Merge算法的比较次数≤2个子序列数据元素个数和-1
- 最坏情况时间复杂度
最坏情况是最后一次比较两个有序子序列各自剩最后一个数据元素。例如
我们基于分治法时间复杂度公式进行分析
则我们有分析公式如下
则我们可以大胆得出结论,归并排序最坏情况时间复杂度为
2. 最好情况时间复杂度
最好情况时间复杂度也是
最好情况与最坏情况的时间复杂度都是nlogn量级的,那么我们也很容易得出结论(类比与高等数学的夹逼准则)归并排序的平均时间复杂度也为nlogn量级。
三、归并排序改进措施
- 递归:消除递归,避免递归过程的时间消耗。
- 最长无逆序子序列:我们经过分析知道,归并排序的基础是两个有序子序列的合并,那么我们可以通过寻找最长无逆序子序列来优化归并排序的比较次数。例如,(4,5,6,3,7,1)这个序列,我们找到三个无逆序子序列,直接对这三个子序列进行合并即可,减少比较次数。
- 小排序问题:划分为小序列,做直接插入排序,再采用归并排序。
- 不回写:这个策略是最重点要讲述的策略。我们上面两段示例代码中都存在从B写回E的操作,这种写回操作在大排序问题时非常浪费时间,我们就思考一种不回写策略来解决这个问题。
详细过程:
思路:奇数趟从E[]写到B[]数组,偶数趟从B[]写到E[]数组。如果共做了奇数趟,排序结束,则最多回写一次。
基于这个思路,我们将二路归并排序的代码做一些小小的修改,以满足我们的不回写策略。这和不回写策略也同时消除了递归过程。下面是示例代码。
public void Merge1(int[] E, int[] B, int s, int m, int t) {
int k, p, j = 0;
for (k = s, p = s, j = m + 1; p <= m && j <= t; ++k) {
if (E[p] <= E[j]) {
B[k] = E[p++];
} else {
B[k] = E[j++];
}
}
if (p <= m) {
for (int i = p; i <= m; i++) {
B[k++] = E[i];
}
}
if (j <= t) {
for (int i = j; i <= t; i++) {
B[k++] = E[i];
}
}
//这里并没有写回操作
}
public void Msort_no_writeback(int[] E, int n) {
int B[] = new int[E.length];
int len = 1;
int flag = 1;
while (len < n) {
if (flag == 1) {//通过flag来判定当前是奇数回合还是偶数回合
flag = 0;
int i;
for (i = 0; i < n + 1 - 2 * len; i = i + 2 * len) {
Merge1(E, B, i, i + len - 1, i + 2 * len - 1);
}
if (i + len - 1 < n) {
Merge1(E, B, i, i + len - 1, n - 1);
} else {
for (; i < n; i++) E[i] = B[i];
}
len *= 2;
} else {
flag = 1;
int i;
for (i = 0; i < n + 1 - 2 * len; i = i + 2 * len) {
Merge1(B, E, i, i + len - 1, i + 2 * len - 1);
}
if (i + len - 1 < n) {
Merge1(B, E, i, i + len - 1, n - 1);
} else {
for (; i < n; i++) B[i] = E[i];
}
len *= 2;
}
}
if (flag == 1) {
for (int p = 0; p < n; p++) {
E[p] = B[p];
}
}
}