C/C++ 递归
递归
当一个函数调用它自己来定义时称它为递归函数。(什么叫它自己调用它自己呢?)
1.1、引出递归
从一个简单的问题考虑递归,求0,1,2, 3,4,5......n的和。
首先定义一个求和公式:sum(n);
显然对于(n > 0): sum(n) = sum(n - 1) + n ;
(n = 0 ) : sum(0) = 0;
成立。
将上述公式翻译成C++函数:
unsigned int sum(unsigned int n)
{
if(0 == n)
{
return 0; //基准情况(递归的出口),sum不能一直调用它自己吧,总归要有一个出口结束递归吧
}
else
{
return sum(n - 1) + n; //sum(unsigned int)调用了它自己
}
}
假设 n = 5 分析一下计算过程:
sum(5) = sum(4) + 5;
sum(4) = sum(3) + 4;
sum(3) = sum(2) + 3;
sum(2) = sum(1) + 2;
sum(1) = sum(0) + 1;
sum(0) = 0; 当sum(0)时,sum()不再调用它自己,作为递归的出口结束递归。
假设没有n = 0, sum(0) = 0 这个基准情况作为递归的出口跳出递归,递归就会一直递归下去,没完没了直至崩溃。因此递归函数必须有一个基准情况作为递归出口。
1.2、失败的递归
给出一个所谓的递归函数:
int bad(unsigned int n)
{
if(0 == n)
{
return 0;
}
else
{
return bad(n/3 + 1) + n - 1;
}
}
分析一下以上函数,函数给出了 n = 0 的情况作为递归的出口,看似没什么问题。
还是假设n = 5;
bad(5) : 调用bad(5/3 + 1), 即bad(2);
bad(2) : 调用bad(2/3 + 1), 即bad(1);
bad(1) : 调用bad(1/3 + 1), 即bad(1);
bad(1) : 调用bad(1/3 + 1), 即bad(1)..........
bad(1)一直调用bad(1), 一直调用到程序崩溃。很明显bad()函数定义虽然给出了 n = 0 作为递归出口,但是bad()函数根本不会推进到n = 0 的这种情况。因此递归调用必须总能够朝着产生基准情况(递归出口)的方向推进。
1.3、递归和归纳
考虑一个问题:现在需要将一个正整数 n 打印出来,但是I/O给出的函数接口(printDigit)只能处理单个数字(即n < 10)。
我们随便假设一个n值:n = 2019,那么单个数字打印的顺序就是2, 0, 1, 9。换句话说,9是最后一个打印的,在打印9之前要先打印201,即先打印“201”,再打印“9”;依次类推对于“201”先打印“20”,再打印“1”;对于“20”先打印“2”,再打印“0”;对于2已经是单个数字,可以直接打印了, 不需要再划分,再递归了,也就是说单个数字n < 10即为递归的出口。
我们按上述思路细致的分析一下:
对2019分成2部分: 201 = 2019 / 10; 9 = 2019 % 10;
对201分成2部分:20 = 201 / 10; 1 = 201 % 10;
对20分成2部分:2 = 20 / 10; 0 = 20 % 10;
对于 2 满足 n < 10 的条件,不再递归,直接打印。
现在递归已经很明显了,尝试编写一下代码:
//假设printDigit((unsigned int n)如下,
void printDigit(unsigned int n)
{
std::cout << n;
}
void print(unsigned int n)
{
if(n >= 10)
{
print(n / 10);
}
printDigit(n % 10);
}
代码编写好了,现在需要证明以下代码是否正确:对于n >= 0,数的递归打印算法总是正确的。
证明:用k表示数字n的包含单个数字的个数。当k = 1,即 n < 10 时,很明显程序是正确的,因为它不需要递归,print()只调用一次printDigit(), 不调用它自己。然后假设print()对于所有k位数都能正常工作,任何k + 1位的数字n都可以通过它的前k位的数字和最低1位数字来表示。前k 位的数字恰好是[ n / 10], 归纳假设它能正常工作,而最低1位数字是[ n % 10],因此该程序能够正确的打印出任意k + 1位。于是根据归纳法[1],所有数字都能被正确打印出来。
由以上实例总结可以出一条递归的设计法则:假设所有递归调用都能运行。
1.4、递归的合成效益法则
用递归实现一个斐波那契数列:
//斐波纳契数列:1、1、2、3、5、8、13、21、34
int f(int n)
{
if(n < 1)
{
return 0;
}
else if(n <= 2)
{
return 1;
}
return f(n-1) + f(n-2);
}
假设n = 8, 函数调用f(8), 递归调用如下图:
由上图我们不厌其烦的数一下:
n = 1时,f()调用1次;
n = 2时,f()调用1次;
n = 3时,f()调用3次;
n = 4时,f()调用5次;
n = 5时,f()调用9次;
n = 6时,f()调用15次;
n = 7时,f()调用25次;
n = 8时,f()调用41次;
增长的是不是太快了,在f()里加一个计数器测试一下,可以看到在n = 30 的时候,f()的调用次数大约在160万。
究其原因,是因为我们在求解的过程时,重复了大量的计算过程, 在n = 8 的时候单单是f(3)就重复调用了8次。
由上我们可以得出一个结论:在求解一个问题的同一实例时,在不同的递归中做重复性的工作,对资源的消耗可能是灾难性的。
最后归纳一下要牢记的递归四条基本法则:
- 基准情形。必须总有某些基准情况,它无须递归就能求解,即递归必须有出口。
- 不断推进。对于那些需要递归求解的情形,每一次递归调用都必须要使求解状态朝基准情形的方向推进。
- 设计法则。假设所有的递归调用都能运行。
- 合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归中做重复性的工作。
1、证明当n= 1时命题成立。2、假设n=m时命题成立,那么可以推导出在n=m+1时命题也成立。(m代表任意自然数)。3、归纳结论。 ↩︎