递归(Recursion)简述及一些注意事项
《Data Structure and Algorithm Analysis in C++》笔记
大多数的数学函数可以被描述成简单表达式。
例如:
华氏度和摄氏度转换的表达式为
C = 5 *( F - 32 ) / 9
这种式子我们可以明确地一行行转换成C++代码。
但有时候,数学函数的表达式采用另一种形式——递推式(iterative)。
例如:
f(0) = 0
f(x) = 2 * f(x - 1) + x2
计算几项结果:
f(1) = 1,f(2) = 6, f(3) = 21 ...
递推式通常给出初值和条件,后项由前项加以条件得到结果,层层进行以得到各项。
对于递推式,C++语言可以通过两种思路处理。
其中一种是递归(recursive)。
C++允许函数被递归式定义,但这种允许是有条件的。
并非所有数学上的递推式都可以高效地(或者正确地)使用C++模拟。
出于效率考虑,我们理想中的递归实现应当保证后项依赖的前项不涉及复杂计算。
上述递推式的C++语言描述:
int f(int x) {
if (x == 0) {
return 0;
}
else {
return 2 * f(x - 1) + x*x;
}
}
其中第2到3行是递推式f(0)的描述,它在递推式中叫做初始条件,而在递归实现中称为递归出口。
仅仅声明f(x) = 2f(x-1) +x2的关系是无意义的,递归实现必须先给出明确的递归出口。
对于递归,通常会存在普遍的疑问。
其中一个共同的问题就是:难道这不会陷入绕圈逻辑?如何避免呢?
答案是:
只要我们在给定递推关系时,保证递推关系仅仅依赖于前后项而非自身即可。
换句话:
递推f(5)需要用到计算f(5)的结果,那将会陷入绕圈。
但递推f(5)仅需要用到计算f(4)的结果,不会绕圈。
另外一个重要问题,检查递归出口有效性。(小心负数,小心除法)
在上述递归实现中,
想知道f(3),就得知道f(2)。想知道f(2),就得知道f(1)。想知道f(1),就得知道f(0)。而f(0)=0是已知的。
这使得其看上去像是个美妙的递归实现。
小心负数:
但假如我们试图计算符合数据类型的f(-1)呢?
想知道f(-1),就得知道f(-2)。想知道f(-2),就得知道f(-3)。……
以此类推,想得到前项需要用到无限的后项,计算机永远也得不到结果,这种实现明显是不合格的。
另外的导致递推出口失效的隐秘可能,除法运算:
int bad(int n) {
if (n == 0) {
return 0;
}
else {
return bad(n / 3 + 1) + n - 1;
}
}
它看上去定义了明确的递推出口,但bad(1) 需要知道bad(n / 3 + 1),而C++的除法运算会隐式转换,1/3 =0.
bad(1)需要知道bad(1),bad(2)需要知道bad(1),而bad(3)到bad(5)都需要知道bad(2)。
而这种混乱关系中,我们仅仅知道bad(0) = 0这个无法抵达的出口,因此整个递归实现都失效了。
在递推关系涉及到负数和除法运算时,我们要格外小心。
不止一个但同一类出口:
在设备一次只允许输出一位的情况下,按位输出完整数字,
void printNum(int n) { if (n>=10) { printNum(n / 10); } std::cout << n % 10; }
那么出口将是
if(0<=n<10){
将该数字输出
}
也即0~9内任意可能的一个数字。
而虽然其出口导向的结果不为一个,但判定其不会绕圈是因为,若划分族群,每一个大的数的族,都将由明确的小同族的数确定。