分治法
许多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或者多次递归地调用其自身以解决紧密相关的若干子问题。这些算法典型地遵循分治法的思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
分治模式在每层递归时都有三个步骤:
分解原问题为若干子问题,这些子问题是原问题的规模较小的实例
解决这些子问题,递归地求解各子问题。然后若干子问题的规模足够小,则直接求解。
合并这些子问题的解成原问题的解。
归并排序算法完全遵循分治模式
直观上其操作如下:
分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列。
解决:使用归并排序递归地排序两个子序列。
合并:合并两个已排序的子序列以产生已排序的答案。
当待排序的序列长度为1时,递归“开始回升”,在这种情况下不需要做任何工作,因为长度为1的每个序列都已排序好。
归并排序算法的关键操作是“合并”步骤中两个已排序序列的合并。我们通过调用一个辅助过程MERGE(A,p,q,r)来完成合并,其中A是一个数组,p、q和r是数组下标,满足p≤q<r。该过程假设子数组A[p..q]和A[q+1..r]都已排序好。它合并这两个子数组形成单一的已排好序的子数组并代替当前的子数组A[p..q]。
过程MERGE需要Θ(n)的时间,其中n=r-p+1是待合并元素的总数。以我们玩扑克牌为例,假设桌子上有两堆牌面朝上的牌,每堆都已排序,最小的牌在顶上。我们希望把这两堆牌合并成单一的排好序的输出堆,牌面朝下地放在桌子上。我们的基本步骤包括在牌面朝上的两堆牌的顶上两张牌中选取较小的一张,将该牌从其堆中移开(该堆的顶上将显露一张新牌)并牌面朝下地将该牌放置到输出堆。重复这个步骤,直到一个输入堆为空,这时,我们只要拿起剩余的输入堆并牌面朝下地将该堆放置到输出堆。因为我们只是比较顶上的两张牌,所以计算上每个基本步骤需要常量时间。因为我们最多执行n个基本步骤,所以合并需要Θ(n)的时间。
下面的伪代码实现了上面的思想,但有一个额外的变化,以避免在每个基本步骤必须检查是否有堆为空。在每个堆的底部放置一张哨兵牌,它包含一个特殊的值,用于简化代码。这里我们使用∞作为哨兵值,结果每当显露一张值为∞的牌,它不可能为较小的牌,除非两个堆都已经显露出其哨兵牌。但是,一旦发生这种情况,所以一旦已执行r-p+1个基本步骤,算法就可以停止。
MEGREG伪算法
MERGET(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] = ∞
9 R[n2+1] = ∞
10 i = 1
11 j = 1
12 for k=p to r
13 if L[i] ≤ R[j]
14 A[k] = L[i]
15 i = i + 1
16 else
17 A[k] = R[j]
18 j = j + 1
当子数组A[9..16]包含序列<2,4,5,7,1,2,3,6>时,调用MERGE(A,9,12,16)第10~18行的操作。在复制并插入哨兵后,数组L包含<2,4,5,7,∞>,数组R包含<1,2,3,6,∞>。A中的浅阴影位置包含它们的最终值,L和R中的浅阴影位置包含有待于被复制回A的值。合在一起,浅阴影位置总时包含原来在A[9..16]中的值和两个哨兵。A中的深阴影位置包含将被覆盖的值,L和R中的深阴影位置包含已被复制回A的值。(a)~(h)在第12~18行循环的每次迭代之前,数组A、L和R以及它们各自的下标k、i和j。(i)终止时的数组与下标。这时,A[9..16]中的子数组已经排好序,L和R中的两个哨兵是这两个数组中仅有的两个未被复制回A的元素
过程MERGE的详细工作过程如下:
第一行计算子数组A[p..q]的长度n1
第二行计算子数组A[q+1..r]的长度n2
第三行我们创建长度分别为n1+1和n2+1的数组L和R,每个数组中额外的位置将保存哨兵。
第四到五行的for循环将子数组A[p..q]复制到L[1..n1+1]
第六到七行的for循环将子数组A[q+1..r]复制到R[1..n2+1]
第八到九行将哨兵放到数组L和R的末端
第十到十八行通过维持以下循环不变式,执行r-p+1个基本步骤:
在开始第十二到十八行for循环的每次迭代时,子数组A[p..k-1]按从小到大的顺序包含L[1..n1+1]和R[1..n2+1]中的k-p个最小元素。进而,L[i]和R[j]是各自所在数组中未被复制回数组A的最小元素。
我们必须证明第十二到十八行for循环的第一次迭代之前该循环不变式成立,该循环的每次迭代保持该不变式,并且循环终止时,该不变式提供了一种有用的性质来证明正确性。
初始化:循环的第一次迭代之前,有k=p,所以子数组A[p..k-1]为空。这个空的子数组包含L和R的k-p=0个最小元素。又因为i=j=1,所以L[i]和R[j]都是各自所在数组中未被复制回数组A的最小元素。
保持:为了理解每次迭代都维持循环不变式,首先假设L[i]≤R[j]。这是L[i]是未被复制回数组A的最小元素。因为A[p..k-1]包含k-p个最小元素,所以在第十四行将L[i]复制到A[k]之后,子数组A[p..k]将包含k-p+1个最小元素。增加k的值和i的值后,为下次迭代重新建立了该循环不变式。反之,若L[i]>R[j],则第十七到十八行执行适当的操作来维持该循环不变式。
终止:终止时k=r+1。根据循环不变式,子数组A[p..k-1]就是A[p..r]切按从小到大的顺序包含L[1..n1+1]和R[1..n2+1]中的k-p=r-p+1个最小的元素。数组L和R一起包含n1+n2+2 = r-p+3个元素。除了两个最大的元素以外,其他所有元素都已被复制回数组A,这两个最大的元素就是哨兵。
现在我们可以把过程MERGE作为归并排序算法中的一个子程序来用。下面的过程MERGE-SORT(A,p,r)排序子数组A[p..r]中的元素。若p≥r,则该子数组最多有一个元素,所以已经排好序。否则分解步骤简单地计算一个下标q,将A[p..r]分成两个子数组A[p..q]和A[q+1..r],前者包含n/2个元素,后者包含n/2个元素。
MERGE-SORT(A,p,r)
1 if p < r
2 q = [(p+r)/2]
3 MERGE-SORT(A,p,q)
4 MERGE-SORT(A,q+1,r)
5 MERGE(A,p,q,r)
为了排序整个序列A=<A[1],A[2],...,A[n]>,我们执行初始调用MERGE-SORT(A,1,A.length),这里再次有A.length=n,下图说明了当n为2的幂时该过程的操作。算法由以下操作组成:合并只含1项序列对形成长度为2的排好序的序列,合并长度为2的序列对形成长度为4的排好序的序列,依次下去,直到长度为n/2的两个序列被合并最终形成长度为n的排好序的序列。
C语言代码实现
伪算法修改
由于数组下标从0开始,所以修改伪算法如下
MERGET(A,p,q,r)
1 n1 = q - p + 1
2 n2 = r - q
3 let L[0..n1] and R[0..n2] be new arrays
4 for i = 0 to n1-1
5 L[i] = A[p+i-1]
6 for j = 0 to n2-1
7 R[j] = A[q+j]
8 L[n1] = ∞
9 R[n2] = ∞
10 i = 0
11 j = 10
12 for k=p-1 to r
13 if L[i] ≤ R[j]
14 A[k] = L[i]
15 i = i + 1
16 else
17 A[k] = R[j]
18 j = j + 1
C代码实现
#include <stdio.h>
#include <stdlib.h>
void merge(int *A, int p, int q, int r);
void merge_sort(int *A, int p, int r);
void printf_array(int *A, int r);
int main(void)
{
int A[] = {2,4,5,7,1,2,3,6};
int r = sizeof(A)/sizeof(A[0]);
printf("Before Sortting: ");
printf_array(A, r);
merge_sort(A, 1, r);
printf("After Sortting: ");
printf_array(A, r);
return 0;
}
void merge(int *A, int p, int q, int r)
{
int i = 0;
int j = 0;
int k = 0;
int n1 = q - p + 1;
int n2 = r - q;
int L[n1+1];
int R[n2+1];
for(i = 0; i < n1; i++)
{
L[i] = A[p-1+i];
//printf("L[%d]=%d\n",i,L[i]);
}
for(j = 0; j < n2; j++)
{
R[j] = A[q + j];
//printf("R[%d]=%d\n",j,R[j]);
}
L[n1] = 100;
R[n2] = 100;
i = 0;
j = 0;
for (k = p-1; k <= r -1; k++)
{
if(L[i] <= R[j])
{
A[k] = L[i];
//printf("A[%d]=%d\n",k,A[k]);
i = i + 1;
} else {
A[k] = R[j];
//printf("A[%d]=%d\n",k,A[k]);
j = j + 1;
}
}
return ;
}
void merge_sort(int *A, int p, int r)
{
int q;
if(p < r)
{
q = (p + r) / 2;
//printf("Function:%s Line:%d p=%d q=%d r=%d\n",__func__,__LINE__,p,q,r);
merge_sort(A,p,q);
merge_sort(A,q+1,r);
merge(A,p,q,r);
}
}
void printf_array(int *A, int r)
{
int p = 0;
for(p = 0; p < r; p++)
printf("%d ",A[p]);
printf("\n");
}
结果测试
root@ubuntu:/home/test# ./a.out
Before Sortting: 2 4 5 7 1 2 3 6
After Sortting: 1 2 2 3 4 5 6 7