递归与分治策略
文章目录
一、递归
1.区别递归与循环
递归:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这你把钥匙打开了几扇门。
循环:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案。
二、分治
1.基本思想
分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
如果原问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
2.适用情况
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
2.基本步骤
分治法在每一层递归上都有三个步骤:
step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
step3 合并:将各个子问题的解合并为原问题的解。
它的一般的算法设计模式如下:
Divide-and-Conquer(P)
1. if |P|≤n0
2. then return(ADHOC(P))
3. 将P分解为较小的子问题 P1 ,P2 ,...,Pk
4. for i←1 to k
5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi
6. T ← MERGE(y1,y2,...,yk) △ 合并子问题
7. return(T)
其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,...,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,...,Pk的相应的解y1,y2,...,yk合并为P的解。
3.递推方程的求解方法
1、迭代法
不断用递推方程的右部替换左部,下面以汉诺塔为例进行求解。
在这里插入图片描述
有时候直接迭代可能不太方便,可以使用换元迭代。下面以二分归并排序迭代方程为例进行求解。
在这里插入图片描述
2、差消法
差消法一般应用在递归方程右边不仅仅只依赖于当前项的前一项,而是前很多项,这种递归方程直接用迭代法很麻烦。属于高阶递归方程,因此要先把高阶递归方程进行差消,再进行迭代。以快速排序的递归方程为例。
在这里插入图片描述
3、递归树
建立递归树,每次迭代将函数项作为儿子,非函数项作为根的值。以二分归并排序递归方程为例。
在这里插入图片描述
4、主定理
在这里插入图片描述
下面举一个例子:
4.算法复杂度分析
一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:
T(n)= k T(n/m)+f(n)
三、排序问题
1.合并排序
1.概念
归并排序(Merge Sort)是一种高效的、通用的、基于比较的分治排序算法。大多数实现都产生了稳定的排序,意味着实现保留了排序输出中想等元素的输入顺序。归并排序是一种分治算法,由冯.诺伊曼于1945年发明。
2.基本思想
将待排序元素分成大小大致相同的两个相同子集合,分别对两个子集合进行排序,最终将排好序的子集合合并成要求的排好序的集合。
输入: 一系列的无序元素(eg.数字)组成的输入数组A。
经过: 分两步。第一步、先将整个问题递归分解成一个个只包含一个元素的子问题/子数组(eg,刚开始问题是{2,1,5,6,4,10},直接分成2,1,5,6,4,10即可)
第二步、一步步将分解出来的子数组合并成一个排好序的数列,向两两对比每个自数组(只有1个元素),按照大小顺序合并成一个子数组(有2个元素);再
两两对比现在的子数组,按照大小顺序合并成一个新的子数组…依此类推,知道最后两个子数组进行对比,里面元素按照大小顺序合并成最终的有序序列。
输出: 输出数组B,里面包含的元素都是A中的但是已经按照要求排好了顺序。
当数组元素个数为奇数时:
当数组元素个数为偶数时:
3.工作原理
3.1.工作流程
1.将未排序的列表划分成为n个子列表,每个子列表包含一个元素(一个元素的列表被认为是排序的)。
2.反复合并子列表以产生新的排序子列表,直到只剩一个子列表。
3.2.划分的方式
自顶向下
自顶向下的实现是使用递归的原理。它从书的顶端开始,然后向下操作,每次操作都问同样的问题(我需要做什么来排序这个数组?),并且回答它(分成两个子数组,进行递归调用,合并结果),知道我们到达树的底部。
自底向上
自底向上的实现则不需要递归。它直接从树的底部开始,然后通过遍历这些片段再将它们合并起来。
其实,任何的合并排序都可以被可视化为树上的操作。树中的叶子代表数组中的各个元素。树的每个内部节点对应于将两个较小的数组合并成一个更大数组。
4.代码实现
递归版
#include<bits/stdc++.h>
#define Maxn 10001
using namespace std;
void Merge(int arr[], int l, int mid, int r);
void MergeSort(int arr[], int l, int r);
void displayArray(int arr[], int n);
int main()
{
int arr[Maxn];
cout << "请输入数组元素的个数: " << endl;
int n;
cin >> n;
cout << "请在" << n << "行内分别输入一个数组元素" << endl;
for(int i = 0; i < n; i++)
cin >> arr[i];
cout << "数组初始状态为: " << endl;
displayArray(arr, n);
MergeSort(arr, 0, n-1);
cout << "数组经过归并排序后的状态为: " << endl;
displayArray(arr, n);
}
void Merge(int arr[], int l, int mid, int r)
{
int i, j, k;
int n1 = mid - l + 1; //左半边的长度
int n2 = r - mid; //右半边的长度
int *L = new int[n1+1]; //开辟临时空间
int *R = new int[n2+1];
for(i = 0; i < n1; i++)
L[i] = arr[i+l]; //下标从0开始 将数组a的元素填入
for(j = 0; j < n2; j++)
R[j] = arr[j+mid+1];
L[n1] = INT_MAX;
R[n2] = INT_MAX;
i = 0;
j = 0;
for(k = l; k <= r; k++) //合并
{
if(L[i] <= R[j])
//注意这里是先把L[i]的值赋给了a[k],之后再i++
//这样是为了方便操作
//下面else语句同理
arr[k] = L[i++];
else
arr[k] = R[j++];
}
}
void MergeSort(int arr[], int l, int r) //p->left r->right
{
if(l < r)
{
int mid = (l + r) / 2; //q为中点
MergeSort(arr, l, mid); //完成左半部的sort
MergeSort(arr, mid+1, r); //完成右半部的sort
Merge(arr, l, mid, r); //将两半部的sorted的数列整合成一个数组排序
}
}
void displayArray(int arr[], int n)
{
for(int i = 0; i < n-1; i++)
cout << arr[i] << " ";
cout << arr[n-1] << endl;
}
非递归版(之后有空再补)
5.算法分析
分类:排序算法
算法种类:分治算法
数据结构:数组
最优时间复杂度:O(nlog(n))
最坏时间复杂度:O(nlog(n))
平均时间复杂度:O(nlog(n))
最坏空间复杂度:共O(n),辅助O(n);当使用linked list,辅助空间为O(1)
稳定性:稳定
复杂性:较复杂