分治法(divide and conquer method)是最著名的算法设计技术,在计算机科学领域孕育了许多重要的和有效的算法。作为解决问题的一般性策略,分治法在政治和军事领域也是克敌制胜的法宝。
分治法将一个难以直接解决的大问题划分成一些规模较小的子问题,分别求解各个子问题,再合并子问题的解得到原问题的解。一般来说,分治法的求解过程由以下三个阶段组成: (1)划分:把规模为 n 的原问题划分为 k 个(通常 k = 2)规模较小的子问题。 (2)求解子问题:分别求解各个子问题。 (3)合并:把各个子问题的解合并起来。
从大量实践中发现,在进行问题划分时,遵循以下启发式原则: (1)平衡子问题(balancing sub-problom):子问题的规模最好大致相同。 (2)各子问题之间相互独立:如果各子问题不是独立的,分治法需要重复求解公共的子问题,此时虽然也可以用分治法,但一般用动态规划法较好。
归并排序
【问题】归并排序(merge sort)的分治策略是: (1)划分:将待排序序列从中间位置划分为两个长度相等的子序列; (2)求解子问题:分别对这两个子序列进行排序,得到两个有序子序列; (3)合并:将这两个有序子序列合并成一个有序序列。
【想法】一个归并排序的例子
【算法实现】设函数Merge实现合并操作,程序如下:
void Merge(int r[ ], int s, int m, int t)
{
int r1[t], i = s, j = m + 1, k = s;
while (i <= m && j <= t)
{
if (r[i] <= r[j]) r1[k++] = r[i++]; //较小者放入r1[k]
else r1[k++] = r[j++];
}
while (i <= m) //处理第一个子序列剩余记录
r1[k++] = r[i++];
while (j <= t) //处理第二个子序列剩余记录
r1[k++] = r[j++];
for (i = s; i <= t; i++) //将合并结果传回数组r
r[i] = r1[i];
}
void MergeSort(int r[ ], int s, int t) //对序列r[s]~r[t]进行归并排序
{
if (s == t) return; //只有一个记录,已经有序
else {
int m = (s + t)/2; //划分
MergeSort(r, s, m); //归并排序前半个子序列
MergeSort(r, m+1, t); //归并排序后半个子序列
Merge(r, s, m, t); //合并两个有序子序列
}
}
快速排序
【问题】快速排序(quick sort)的分治策略是: (1)划分:选定一个记录作为轴值,以轴值为基准将整个序列划分为两个子序列,轴值的位置在划分的过程中确定,并且左侧子序列的所有记录均小于或等于轴值,右侧子序列的所有记录均大于或等于轴值; (2)求解子问题:分别对划分后的每一个子序列递归处理; (3)合并:由于对子序列的排序是就地进行的,所以合并不需要执行任何操作。
【算法实现】设函数Partition实现对序列 r[first] ~ r[end] 进行划分,程序如下:
int Partition(int r[ ], int first, int end)
{
int temp, i = first, j = end;
while (i < j)
{
while (i < j && r[i] <= r[j]) j--; //右侧扫描
if (i < j) {
temp = r[i]; r[i] = r[j]; r[j] = temp; //将较小记录交换到前面
i++;
}
while (i < j && r[i] <= r[j]) i++; //左侧扫描
if (i < j) {
temp = r[i]; r[i] = r[j]; r[j] = temp; //将较大记录交换到后面
j--;
}
}
return i; //返回轴值记录的位置
}
void QuickSort(int r[ ], int first, int end) //快速排序
{
if (first < end)
{
int pivot = Partition(r, first, end); //划分,pivot是轴值的位置
QuickSort(r, first, pivot-1); //对左侧子序列进行快速排序
QuickSort(r, pivot+1, end); //对右侧子序列进行快速排序
}
}