分治法的设计思想
对于一个规模为n的问题:若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模娇小的子问题,这鞋子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并到原问题的解。
这种算法设计策略叫分治法
适用分治法的问题的特征
(1)该问题的规模缩小到一定的成都就可以容易地解决。
(2)该问题可以分解为若干个规模较小的相同问题。
(3)利用该问题分解出的子问题的解可以合并为该问题的解。
(4)该问题所分解出的各个子问题都是相互独立的,即子问题之间不包含公共的子问题。
分治法的求解过程
分治法通常采用递归算法设计技术,在每一层递归上都有3个步骤:
1、分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题。
2、求解子问题:若子问题规模较小而且容易被解决则直接求解,否则递归地求解各个子问题。
3、合并:将各个子问题的解合并为原问题的解。
许多问题可以取k=2,成为二分法,这种使子问题规模大致相等的做法是出自一中平衡子问题的思想,它几乎总是比子问题的做好要好。
归并排序
归并排序的基本思想是:首先将a[0,1,…,n-1]看成是n个长度为1的有序表,将相邻的k(k>2)个有序子表成对归并,得到n/k个长度为k的有序子表;然后再将这些有序子表继续归并,得到n/kk个长度为kk的有序子表,如此反复进行下去,最后得到一个长度为n的有序表。
若k=2,即归并在相邻的两个有序子表中进行的,称为二路归并排序。若k>2,即归并操作在相邻的多个有序子表中进行,则叫多路归并排序。
// 二路归并排序算法
#include <stdio.h>
#include <malloc.h>
// 辅助函数, 输出整型数组中所有元素
void disp(int a[], int n)
{
int i;
for(i = 0; i < n; i ++)
printf("%d ", a[i]);
printf("\n");
}
// 将a[low..mid]和a[mid+1..high]两个相邻的有序子序列归并为一个有序子序列a[low..high]
void Merge(int a[],int low,int mid, int high)
{
int *tmpa; // 用tmpa所指向的这段内存空间临时保存归并好的序列, 最后拷贝回数组a的对应位置
int i = low, j = mid + 1, k = 0; //k是tmpa的下标,i、j分别为两个子表的下标
tmpa = (int *)malloc((high-low+1) * sizeof(int)); //动态分配空间
while(i <= mid && j <= high) //在第1子表和第2子表均未扫描完时循环
{
// 两个子序列都已经有序
// 对两个子序列从左到右扫描
// 每次都挑选最小的那个元素放到tmpa中
if(a[i] <= a[j]) //将第1子表中的元素放入tmpa中
{
tmpa[k] = a[i];
i ++;
k ++;
}
else //将第2子表中的元素放入tmpa中
{
tmpa[k] = a[j];
j ++;
k ++;
}
}
// 经过上述扫描, 肯定还有一个子序列中还有元素没有放到tempa
// 下面两个while循环只会有一个执行
// 如果第一个子序列中还有元素(第二个子序列肯定已扫描完)
while(i <= mid) //将第1子表余下部分复制到tmpa
{
tmpa[k]=a[i];
i ++;
k ++;
}
// 如果第二个子序列中还有元素(第一个子序列肯定已扫描完)
while(j <= high) //将第2子表余下部分复制到tmpa
{
tmpa[k] = a[j];
j ++;
k ++;
}
for(k = 0, i = low; i <= high; k ++,i ++) //将tmpa复制回a中
a[i] = tmpa[k];
free(tmpa); //释放tmpa所占内存空间
}
// 一趟二路归并排序
// a: 待排序的数组
// length: 子序列的长度
// n: 数组a中元素数量
void MergePass(int a[], int length, int n)
{
int i;
// i = 0时, 归并a[0...length - 1]和a[length...2 * length - 1]
// 相当于 a[i...i + length - 1]和a[i + length...i + 2 * length - 1]
// i = i + 2 * length = 2 * length
// i = 2 * length时, 归并a[2 * length...3 * length - 1]和a[3 * length...3 * length - 1]
// 相当于 a[i...i + length - 1]和a[i + length...i + 2 * length - 1]
// i = i + 2 * length = 4 * length
// ...
// 所以, 对于每个i值,
// 第一个子序列的起始下标为: i
// 第一个子序列的结束下标为: i + length - 1
// 第二个子序列的结束下表为: i + 2 * length - 1
//
// 此处, 循环条件的判断: i + 2 * length - 1 < n
// 表示, 对于任何i值, 第二个子序列的下标最多到n - 1(数组最后那个元素的下标)
//
// 一旦这个条件不满足, 则可能的情况:
// (1) 所有子序列正好合并完, n正好是2*length的整数倍, 循环结束时i的值为n, 无需
// 继续归并
//
// (2) 数组a中还剩下一个(完整或不完整的)子序列: a[i...n-1], 循环结束时的i值满足
// i + length - 1 >= n - 1, 无需继续归并
//
// (3) 数组a中还剩下两个子序列, 第一个子序列完整: a[i...i+length-1],
// 第二个子序列不完整: a[i+length...n-1], 循环结束时的i值满足
// i + length - 1 < n - 1
for(i = 0; i + 2 * length - 1 < n; i = i + 2 * length) //归并length长的两相邻子表
Merge(a, i, i + length - 1, i + 2 * length - 1);
if(i + length < n) // 余下两个子表,后者长度小于length
Merge(a, i, i + length - 1, n - 1); //归并这两个子表
}
void MergeSort(int a[], int n) //二路归并算法
{
int length;
// 子序列长度从1开始
// 每趟归并后, 子序列长度变为原来的2倍
// 一旦子序列长度length>=n, 说明上一趟归并已经完成排序, 循环结束
for(length = 1; length < n; length = 2 * length)
MergePass(a, length, n);
// length = 2^0, 2^1, ..., 2^k
// 假设2^(k+1) >= n > 2^k, 则循环了k+1次(从0到k共k+1次)
// => k + 1 >= logn => k >= logn - 1
// k < logn
// => logn - 1 <= k < logn
// => logn <= (k+1) < logn + 1
// 所以循环执行了logn(向上取整)次
// 每次循环对应的归并过程中, 元素比较次数不超过n-1次
// 算法复杂度为O(nlogn)
}
int main()
{
int n = 10;
int a[] = {2, 5, 1, 7, 10, 6, 9, 4, 3, 8};
printf("排序前:");
disp(a, n);
MergeSort(a, n);
printf("排序后:");
disp(a, n);
return 0;
}