要理解归并排序的思想,首先要了解分治的思想:将原问题分解为几个规模较小但类似于原问题的字问题,递归地求解这些字问题,然后再合并这些字问题的解来建立原问题的解。
分治模式在每层递归时都有三个步骤:
- 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例。
- 解决这些子问题,递归的求解各子问题。然而,若子问题的规模足够小,则直接求解。
合并这些子问题的解成原问题的解。
归并算法完全遵循分治模式,直观上其操作如下:1.分解:分解待排序的n个元素的序列成各具n/2个元素的两个字序列。
2.解决:使用归并排序递归地排序两个字序列。
3.合并:合并两个已排序的子序列以产生已排序的答案。当待排序的序列长度为1时,递归“开始回升”,在这种情况下不要做任何工作,因为长度为1的每个序列都已经排好序。
归并算法的关键操作时合并步骤中的两个已排序序列。我们通过调用一个辅助的merge(a, p,q,r)来完成合并。过程merger需要O(n)的时间,其中n = r-p+1是待合并元素的总数。假设a[p…q], a[q+1…r]都是已经排好序的数组,我们要将两个子数组合并成单一的已排好序的数组a[p…r],只需要按序比较两个数组的值,将小的放入目标数组中,直到一个数组为空,将另一个数组剩下的值加入到目标数组的末尾。因为我们最多执行n个步骤,所以合并需要O(n)的时间。
下面给出的伪代码,加入了两个哨兵,用于判断元素是否为空。
merge(a, p, q, r)
1. n1 = q-p+1
2. n2 = r-q;
3. let L[1..n1+1]and R[1..n2+1] be new arrays
4. for i = 1 to n1
5. L[i] = a[p+i-1]
6. for j=1 to n2
7. R[j] = a[q+j]
8. L[n1+1] = INT_MAX
9. R[n2+1] = INT_MAX
10. i = 1, j = 1
11. for k = p to r
12. if L[i] <= R[j]
13. a[k] = L[i]
14. i++
15. else a[k] = R[j]
16. j++
下面先给出C语言的代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int a[1000];
int L[1000];
int R[1000];
#define maxn 100000;
void merge(int *a, int p, int q, int r)
{
int n1 = q-p +1;
int n2 = r-q;
int i, j;
for(i = 1; i <= n1; i++)
{
L[i] = a[p+i-1];
}
for(j = 1; j <= n2; j++)
{
R[j] = a[q+j];
}
L[n1+1] = maxn;
R[n2+1] = maxn;
i = 1, j = 1;
for(int k = p; k <= r; k++)
{
if(L[i] <= R[j])
{
a[k] = L[i];
i++;
}
else {
a[k] = R[j];
j++;
}
}
for(int l = 1; l <= r; l++)
{
printf("%d ", a[l]);
}
printf("\n");
}
void merge_sort(int *a, int p, int r)
{
if(p < r)
{
int q = (p+r)/2;
merge_sort(a, p, q);
merge_sort(a, q+1,r);
merge(a, p, q, r);
}
}
int main()
{
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
}
merge_sort(a, 1, n);
for(int j = 1; j <= n; j++)
{
printf("%d ", a[j]);
}
return 0;
}
输出结果:
完整的执行过程可以参考下面的图:
归并排序算法分析:
下面我们分析建立归并排序n个数的最坏情况运行时间T(n)的递归式。归并排序一个元素需要常量时间。当有n>1个元素时,我们分解运行时间如下:
分解:分解步骤仅仅计算子数组的中间位置,需要常量时间,因此,D(n) = O(1)。
解决:我们递归求解两个规模均为n/2的子问题,将贡献2T(n/2)的运行时间。
合并:我们已经注意到在一个具有n个元素的子数组上过程merge需要O(n)的时间,所以C(n)= O(n)
所以最坏情况运行时间T(n)的递归式:
T(n) = 2T(n/2)+ O(n) 若n>1
为了直观的求解T(n),我们将递归式重写成
T(n) = 2T(n/2) +cn 若n>1
其中c代表求解规模为1的问题所需的时间以及在分解步骤与合并步骤处理每个数组元素所需的时间。下面图显示了求解递归式的过程,我们假设n刚好是2的幂。图(a)部分图示了T(n),它在(b)部分被扩展成一课描绘递归式的等价树。项cn是树根(在递归的顶层引起的代价),根的两棵子树是两个较小的递归式T(n/2)。(c)部分显示了通过扩展T(n/2)再推进一步的过程。在第二层递归中,两个子结点中每个引起的代价都是cn/2。我们通过将其分解成由递归式所确定的它的组成部分来继续扩展树中的每个结点。直到问题规模下降到1,每个子问题只要代价c。(d)部分图示了结果递归树。
从(d)中我们可以看出,完全扩展了的递归树具有lgn+1层,每层将贡献总代价cn,所以,总代价为cnlgn+cn,也就是O(nlgn)。