递归(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内任意可能的一个数字。

而虽然其出口导向的结果不为一个,但判定其不会绕圈是因为,若划分族群,每一个大的数的族,都将由明确的小同族的数确定。

posted on 2018-07-29 11:47  jyunlon  阅读(1060)  评论(0编辑  收藏  举报

导航