您的位置:首页 > 其它

研究生课程 算法分析-分治法

2016-12-29 19:46 316 查看
分治法(divide and conquer)是算法分析里比较直观和朴素的思想,应用也很广泛。

分治法的思想

分治法的思想是,把一个复杂的问题 P 划分称 k 个子问题,这些子问题相互独立且与原问题相同。递归调用子问题,直到问题规模足够小,可以很容易地求解为止;接着,把小规模的问题的解合并成一个更大规模的问题的解。

可以用下面的伪代码来描述,

divide-and-conquer(P) {
if (|P| <= n0) adhoc(P); // 小规模问题求解
divide P into smaller subinstances P1, P2,...,Pk;  // 分解问题成小规模问题
for (i = 1; i <= k; ++ i)
yi = divide-and-conquer(Pi);     // 递归解决子问题
return merge(y1, y2,...,yk);      // 合并为原问题的解
}


分治法和递归

从分治法的思想可以看出,在求解子问题的时候往往需要用到递归,因此分治法和递归经常一起出现。递归算法的优点是结构清晰,可读性强,一般代码也比较容易写,但是缺点是运行效率很低,无论是计算时间还是占用的存储空间都很大。

一般有下面几种解决的办法,

用自定义栈,做了编译器做的事,优化效果不明显。

用递推的方法来实现递归函数

通过变换,将一些递归转化成尾递归(尾递归是所有子递归出现在递归函数的尾部),迭代求结果

后两种方法虽然范围有限,但是如果可以在问题中这样优化的话,效果会比较好。

分治法的适用条件

看一个问题是否可以用分治法求解,可以考虑这个问题是否符合下面几种条件,

小规模时,问题容易求解,这个一般都很容易满足。

问题具有 最优子结构 的性质,即可以分解成若干个规模较小的相同问题。

子问题的解可以 合并为该问题的解

子问题相互独立,即 不包含公共的子问题

满足前三条特征,可以用分治法;如果不具备第三条特征,可以考虑贪心算法和动态规划。如果问题满足第 4 个特征的话,分治法会重复计算导致效率,用动态规划较好。

分治法的应用

二分查找算法

二分查找要解决的问题是,从有序数组 a 中找出特定元素 x 出来。因为是有序,所以可以用考虑分治法,从线性扫描的复杂度 O(n) 优化到 O(logn)。

有递归和非递归两种实现方法。

大整数的乘法

大整数的乘法,分治成较小规模的子问题后,从四次乘法的优化到三次乘法,可以把复杂度从 O(n2) 优化到 O(nlog3=1.59),有较大的改进。

Strassen 矩阵乘法

与大数乘法的例子类似,做分治后想办法减少乘法的次数,从 O(n3) 优化到了 O(nlog7=2.81) 的复杂度。

归并排序

基本思想是,把待排序的数组分成两个子集,分别排序后再合并。递归代码如下,

void merge_sort(int a[], int start, int end) {
if (start < end) {
int mid = (start + end) / 2;
merge_sort(a, start, mid);     // 对左边的序列递归排序
merge_sort(a, mid + 1, end);   // 对右边的序列递归排序
merge(a, start, end);          // 合并子序列
}
}


快速排序

快排的想法很简单,是想找一个数作为排序的基准(pivot),小的放左边,大的放右边。然后对左边和右边的两个子问题递归排序。递归实现如下,

void quick_sort(int a[], int start, int end) {
if (start < end) {  //
int pivot_index = partition(a, start, end);  // 挖坑划分
quick_sort(a, start, pivot_index - 1);       // 左边的子序列递归排序
quick_sort(a, pivot_index + 1, end);         // 右边的子序列递归排序
}
}


注意快排的划分函数
partition
,用的是挖坑填坑的方法。以第一个元素为基准(pivot),最后返回 pivot 的位置 pivot_index。此时左边的数组元素都比 pivot 小,右边的数组元素都比 pivot 大。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: