【问题标题】:Tail Recursion in C++ with multiple recursive function calls具有多个递归函数调用的 C++ 中的尾递归
【发布时间】:2012-01-22 20:38:22
【问题描述】:

我正在阅读这个post 的尾递归。

我将复制发布的解决方案:

unsigned int f( unsigned int a ) {
   if ( a == 0 ) {
      return a;
   }
   return f( a - 1 );   // tail recursion
}

我想知道,如果结果取决于几个递归函数调用怎么办? 例如:

unsigned int f( unsigned int a ) {
    if ( a == 0 ) {
          return a;
       }
    return f(a -1) + f( a - 1 );   
}

上面的代码会被编译器优化吗?

【问题讨论】:

    标签: c++ compiler-optimization tail-recursion


    【解决方案1】:

    就目前而言,尾递归不适用。但是,如果您查看链接到的问题的second answer 的末尾,您可以看到如何适当地重写该函数。以

    开头
    unsigned int f( unsigned int a ) {
        if ( a == 0 ) {
              return a;
        }
        return f(a-1) + f(a-1);   
    }
    

    改写如下:

    unsigned int f( unsigned int a ) {
        if ( a == 0 ) {
              return a;
        }
        return 2 * f(a-1);  
    }
    

    即使是现在,尾递归仍然不能直接应用。我们需要确保返回的格式严格为return f(....)。再次重写函数:

    unsigned int f( unsigned int a, unsigned int multiplicative_accumulator = 1 ) {
        if ( a == 0 ) {
              return multiplicative_accumulator * a;
        }
        return f(a-1, multiplicative_accumulator * 2 );   
    }
    

    现在,尾递归适用。这使用了 multiplicative_accumulator 的默认值(感谢@Pubby),以便对f 的第一次调用可以简单地为f(x),否则您将不得不写一些东西f(x,1)

    感谢@SteveJessop 的最后几点说明:

    • f(a+1)+f(a+1) 更改为2*f(a+1) 是安全的,因为f 没有副作用(打印、修改堆等)。如果 f 确实有副作用,那么重写将无效。
    • 原版相当于(2*(2*(2*a))(或更准确地说,(((a+a)+(a+a))+((a+a)+(a+a)))),而当前版本更像(((2*2)*2)*a)。这很好,特别是对于整数,因为乘法是关联的和分配的。但这与float 不完全等价,您可能会得到小的舍入差异。对于浮点运算,有时a*b*c 可能与c*b*a 略有不同。

    【讨论】:

    • 为什么不使用默认值? unsigned int f( unsigned int a, unsigned int multiplicative_accumulator = 1 )
    • 并注意这段代码是等价的,因为 (a) f 没有副作用,所以函数被调用的次数无关紧要,以及 (b) @987654339 中的乘法@ 在加法中是关联的和分布的。 float(例如)中的乘法不是关联的,因此这种转换不会产生等效的代码。通常你不会关心差异,但你必须对这种事情小心一点。
    【解决方案2】:

    第二个函数不是尾递归的,不能轻易地用循环替换,所以很可能编译器不会这样做。

    【讨论】:

      【解决方案3】:

      这不是尾递归(函数的结果是递归调用的结果):在递归(加法)之后有一个操作要做。有一个更复杂的转换(取决于加法的交换性)来获得尾递归:添加一个带有累加器的辅助函数:

      unsigned int f_helper(unsigned int a, unsigned int acc)
      {
         if (a == 0) {
            return acc;
         }
         return f_helper(a-1, f(a-1)+acc);
      }
      
      unsigned int f(unsigned int a) {
          if (a == 0) {
                return a;
          }
          return f_helper(a-1, f(a-1));
      }
      

      你可以把它变成一个循环

      unsigned int f_helper(unsigned int a, unsigned int acc)
      {
         while (a != 0) {
            acc += f(a-1);
            a = a-1;
         }
         return acc;
      }
      
      unsigned int f(unsigned int a) {
          if (a == 0) {
             return a;
          }
          return f_helper(a-1, f(a-1));
      }
      

      然后放回f中

      unsigned int f( unsigned int a ) {
        if (a == 0) {
            return a;
         }
         unsigned acc = f(a-1);
         a = a-1;
         while (a != 0) {
            acc += f(a-1);
            a = a-1;
         }
         return acc;
      }
      

      【讨论】:

        【解决方案4】:

        我希望它不会像其他人提到的那样被优化,但通常这些类型的问题可以通过使用记忆化来解决,它使用内存而不是进行另一个递归调用。

        有关使用 C++ 的精彩解释,您可以查看 http://marknelson.us/2007/08/01/memoization/

        在您的示例中,最后一次调用可以替换为

        return 2 * f(a - 1);
        

        这将是可优化的。

        在很多情况下,尾递归似乎不起作用,这可能是您只需要查看您的算法,并进行一些更改以帮助实现此优化。

        【讨论】:

          猜你喜欢
          • 2016-03-21
          • 2011-07-19
          • 1970-01-01
          • 1970-01-01
          • 2019-09-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多