本章介绍了一个贯穿本书的框架,后续的算法设计都是在这个框架中进行的。
本章通过插入排序和归并排序两种常见的算法来说明算法的过程及算法分析,在分析插入排序算法时,书中是用了循环不变式证明了算法的正确性,在介绍归并排序时算法过程中引入了分治法的思想。
2.1 插入排序
这是我们的第一个算法(插入排序)用于求解第一章中引入的排序问题:
输入:n个数的一个序列<a1, a2, ..., an>。
输出:输入序列的一个排列<a1', a2', ..., an' >,满足a1‘<=a2'<=...<=an'
插入排序的基本操作是将一个记录插入到以排序好的有序表中,从而得到一个新的记录数增1的有序表。一般情况下,第i趟直接插入排序的操作为:在含有i-1个记录的有序子序列r[1..i-1]中插入一个A[i]后,编程含有i个记录的有序子序列r[1..i],直到把第n个元素插入到n-1个元素中,最终使得n个元素有序。插入排序的为代码如下:
INSERTION_SORT(A)
1 for j=2 to A.length
2 key = A[j]
3 //Insert A[j] into the sorted sequence A[1.. j-1]
4 i = j - 1
5 while(i>0 and A[i]>key)
6 A[i+1] = A[i]
7 i = i - 1
8 A[i+1] = key</span>
循环不变式主要用来帮助我们理解算法的正确性。关于循环不变式,我们必须证明三条性质:
初始化:循环的第一次迭代之前,它为真。
保持:如果循环的某次迭代之前他为真,那么下次迭代之前它仍未真。
终止:在循环终止时,不变式提供了一个有用的性质,该性质有助于证明算法是正确的。
插入排序的证明过程:
初始化:当j=2时,子数组A[1.. j-1]仅由单个元素A[1]组成,实际上就是A[1]原来的元素。而且该子数组是排序好的,这表明第一次循环迭代之前循环不变式成立。
保持:for循环的4-7行将A[j-1], A[j-2], A[j-3]等右移一个位置,直到找到A[j]的适当位置,第8行将A[j]插入该位置。这是子数组A[1.. j]由原来在A[1.. j]中的元素组成,但已经按顺序排列。那么对for循环的下一次迭代增加j将保持循环不变式。
终止:导致for循环终止的条件式j>A.length = n。在循环不变式中将j用n+1代替,我们有:子数组A[1.. n]由原来在A[1.. n]中的元素组成,但已经按顺序排序。因此算法正确。采用c语言编写的插入排序的完整程序如下:
void insertionSort(int *arr, int length){
//判断参数是否合法
if(arr == NULL || 0==length)
{
printf("Check datas or length.\n");
exit(1);
}
//数组下标是从0开始的,从第二个元素(对应下标1)开始向前插入
for(int j=1; j<length; j++)
{
int key = arr[j]; //记录当前要插入的元素
int i = j - 1; //前面已经有序的元素
//寻找待插入元素的位置,从小到到排序,如果是从大到小改为arr[i]<key
while(i>=0 && arr[i]>key)
{
arr[i+1] = arr[i];
i--; //向前移动
}
arr[i+1] = key;
}
}</span>
插入排序的算法分析
过程INSERTION-SORT需要的时间依赖于输入:排序1000个数排序三个数需要更长的时间。此外,依据它们已经被排序的程度,INSERTION-SORT可能需要不同的数量时间来排序一个具有相同规模的输入序列。插入排序算法中,若输入的数组已经排好序,则出现最佳情况。这时对每个j=2, 3, ..., n,程序中的第5行,当i = j - 1时,有A[i]<=key。此时算法的复杂度为O(n)。若输入的数组已反向排序,即按递减序列,则导致最坏情况。此时INSERTION-SORT算法的时间复杂度为O(n^2)。
2.2 归并排序
分治法的思想是:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些问题,然后在合并这些子问题的解来建立原问题的解。
归并排序算法完全遵循分治策略。直观上操作如下:
分解:把具有n个元素的序列分解成具有n/2个元素的两个子序列。
解决:使用归并排序递归地排序两个子序列。
合并:合并两个已排序的子序列则可得到n个元素的排序序列。
归并排序算法的关键操作是“合并”步骤中两个已排序的序列。书中通过调用的一个辅助过程MERGE(A, p, q, r)来完成合并,MERGE算法的伪代码如下(算法在实现的过程中是用来哨兵):
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]
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] = MAX
9 R[n2+1] = MAX
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
算法采用c++ 语言完整程序为:
void Merge(int *arr, int p, int q, int r)
{
int n1 = q - p + 1; //第一个有序子数组元素个数
int n2 = r - q; //第二个有序子数组元素个数
int *Left = (int *)malloc(sizeof(int)*(n1 + 1));
int *Right = (int *)malloc(sizeof(int)*(n2 + 1));
//将子数组复制到临时辅助空间
for(int i=0; i<n1; i++){
Left[i] = arr[p+i];
}
for(int j=0; j<n2; j++){
Right[j] = arr[q+j+1];
}
//添加哨兵
Left[n1] = INT_MAX;
Right[n2] = INT_MAX;
//从第一个元素开始合并
int i = 0;
int j = 0;
for(int k=p; k<=r; k++){
if(Left[i] <= Right[j])
{
arr[k] = Left[i];
++i;
}
else{
arr[k] = Right[j];
++j;
}
}
free(Left);
free(Right);
}
现在我们可以把过程MERGE作为归并排序算法中的一个子程序来用。MERGE-SORT(A, p, r)是实现归并排序的算法:
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)
采用C++完整程序为:
void MergeSort(int *arr, int p, int r)
{
if(p < r){
int q = (p + r) / 2;
MergeSort(arr, p , q);
MergeSort(arr, q + 1, r);
Merge(arr, p, q, r);
}
}
merge过程的运行时间是θ(n),现将merge过程作为归并排序中的一个子程序使用,merge_sort(A,p,r),对数组A[p...r]进行排序,实例分析如下图所示:
归并排序的算法分析
归并排序算法包含对其自身的递归调用,我们可以用递归方程或递归式来描述其运行时间,归并排序算法的最坏情况运行时间T(n)的递归式为:若n>1,T(n) = 2T(n / 2) + Θ(n);若n=1,T(n)= Θ(1)。该递归式可以由主定理求得T(n)的复杂度为Θ(nlgn)。