分治策略是将规模比较大的问题可分割成规模较小的相同问题。问题不变,规模变小。这就自然导致递归过程的产生。
递归是指一个函数能够直接或者间接的调动自己,就称之为递归函数(自己调用自己)。
分治法所能解决的问题一般具有以下特征:
该问题缩小的一定程度就可以容易地解决;
该问题可以分解为若干个规模较小的相同问题;
使用小规模的解可以合并成该问题原规模的解;
该问题所分解出的各个子规模是相互独立的。
分治法步骤:
分解:将问题划分为一些子问题,子问题的形式与原问题一样只是规模更小。
解决:递归地求解子问题,如果子问题的规模足够小,则停止递归,直接求解。
合并:将小规模的解组合成原规模问题的解。
比如说求n!这个问题就适合用分治的策略来求解,我们将一个问题的规模缩小,而问题不变,如下图所示:
我们可以用循环来解决阶乘这个问题,其时间和空间复杂度分别为O(n)和S(1):
int fun(int m)
{
int sum = 1;//在这里要注意整型溢出的情况
//解决办法有两种,一种是加判断是否溢出的条件;
//另外一种是将sum的类型定义为long类型
for (int i = 1; i <= m; i++)
{
sum *= i;
}
return sum;
}
除了循环以外,我们还可以用递归来解决阶乘问题,递归的时间和空间复杂度分别为O(n)和S(n):
nt fac(int n)
{
if (n <= 1) return 1;
else return fac(n - 1) * n;
}
可以看到,递归的代码要比循环代码简洁好多。但是考虑到每递归一次就要为其分配一块栈空间(Windows下的栈大小默认1M),这就导致栈空间的消耗比较大,因此在平常我们解决的问题如果能用循环解决就尽量去使用循环解决。
递归函数的执行分为“递推”和“回归”两个过程,这两个过程由递归终止条件控制,即逐层递归,直至递归终止条件满足,终止递归,然后逐层回归。
递归调用同普通的调用函数一样,每当调用发生时就要分配新的栈帧(形参数据、现场保护、局部变量),而与普通的函数调用不同的是,由于递归的过程是一个逐层调用的过程,因此存在一个逐层连续的分配栈帧的过程,直到遇到递归终止条件时,才开始回归,这时才逐层释放栈帧空间,返回到上一层,直至最后返回到主调函数。
首先就上面阶乘这个例子我们来分析代码层面的递归调用过程,如下图所示:
接着我们来分析一下栈帧的动态调用过程,如下图所示:
小结:
函数被调用,不管是自己调用自己,还是被其它函数调用,都将会给被调用函数分配栈帧。不存在无穷递归(栈空间大小有限)。即递归函数必须要有一个是递归结束的出口(要有递归终止的条件语句)。问题的规模不要太大,递归过深,引起栈溢出。
下面我们使用递归方法逆序打印数组中的元素(注意本段代码在vs2019的编译器下可以通过):
#include <iostream>
#include <vector>
using namespace std;
void Print(const vector<int>& vec, int n)
{
if (n > 0)
{
cout << vec[n - 1] << endl;
Print(vec, n - 1);
}
}
int main()
{
vector <int> vec = { 12,23,34 };
Print(vec, vec.size());
return 0;
}
结果如图
根据上面的图解分析,我们很容易可以知道这个逆序打印是在递归的过程中打印出来的。接下来我们将打印函数里面的两行代码做个调换:
void Print(const vector<int>& vec, int n)
{
if (n > 0)
{
Print(vec, n - 1);
cout << vec[n - 1] << endl;
}
}
我们就会发现它会顺序输出数组中的元素,根据上图解我们也可以分析出它在回归的过程中进行了打印,自然而然也就成了顺序输出。运行结果如下图所示:
需要特别注意的是,当在递归中使用n+/-=1时,尽量不要写成n–/n++/++n/–n这样的前置或后置++,–,很有可能会引发无穷递归导致栈溢出。