为什么阶乘递归函数的效率低于正常的阶乘函数?

时间:2011-10-09 11:47:36

标签: c++ recursion factorial

我有两个函数来计算数n的阶乘。 我不明白为什么'正常'函数需要更少的时间来计算数n的阶乘。 这是正常的功能:

double factorial(int n) {
    double s = 1;
    while (n > 1) {
        s *= n;
        --n;        
    }

    return s;
}

这是递归函数:

double factorial(int n) {
    if (n < 2) return 1;
    return n * factorial(n-1);
}

这应该不那么耗时,因为它不会创建新变量,并且操作更少。 虽然普通函数确实使用了更多的内存,但速度更快。

我应该使用哪一个?为什么?

PS:我正在使用double,因为我需要它来计算e ^ x的泰勒级数。

5 个答案:

答案 0 :(得分:5)

你写的是递归函数“应该不那么耗时,因为它不会创建一个新的变量,并且它会减少操作”。第一个断言是毫无意义的。局部变量的内存通常在进入函数时由单个减法操作分配,这需要不显着的时间(这是人类已知的最快分配)。第二个断言对于C ++实现来说是完全错误的。由于您已经使用编译器测量了递归函数的速度较慢,因此它可以执行更多操作,而不是更少。

现在,为什么。

好吧,每个调用都必须复制一个返回地址和堆栈上的实际参数。这需要时间。另外,为了支持调试和异常,每个函数调用通常会做一些额外的工作来建立一个漂亮的堆栈框架,实质上存储有关堆栈在调用之前的信息。

然而,递归变体不会 更慢。但几乎矛盾的是,实际上可以像迭代一样快的变体看起来会做得更多......想法是编写它以便编译器可以将其转换为迭代版本,也就是说,编译器可以用简单的循环替换递归调用(这需要时间)。

唯一的问题是,据我所知,如果有任何C ++编译器进行这种优化,则很少。 : - (

但是,为了完整性,我们的想法是确保只有一个递归调用,并且它是最后发生的事情。这称为尾递归。您当前的递归代码,

double factorial( int n )
{
    if( n < 2 ) { return 1; }
    return n*factorial( n-1 );
}

不是尾递归的,因为在递归调用之后会乘以n

为了使它具有尾递归性,你可以传递必要的信息来完成最后应该完成的事情,这里*n。所需的信息是n的值,以及它应该完成的事实。这意味着引入一个带有适当形式参数的辅助函数:

double factorialScaledBy( double m, int n )
{
    if( n < 2 ) { return m*1; }

    // Same as "n*factorialScaledBy( m, n-1 )", but tail-recursive:
    return factorialScaledBy( n*m, n-1 );  
}

double factorial( int n )
{
    return factorialScaledBy( 1, n );
}

现在一个足够聪明的编译器可以注意到在递归调用之后在函数执行中不再发生任何事情,因此不使用局部变量,因此它们可以仅用于递归调用,因此可以实现为模拟参数传递加上跳回函数的顶部,即循环。

干杯&amp;第h。,

答案 1 :(得分:4)

我想说这是因为函数调用的时间比while循环更昂贵。我会使用第一个(没有递归)好像N非常大,你将填满你的堆栈并可能得到“堆栈溢出”:)

答案 2 :(得分:3)

最好的办法是不要明确计算因子。如果你正在计算一个exp(x)的泰勒(Maclaurin)系列:

   exp(x) = 1 + x/1! + x^2/2! + x^3/3! + x^4/4! + ...

您最好的选择是做以下事情:

   double y = 1.0;
   int i = 1;
   double curTerm = 1.0;
   double eps = 1e-10;  // whatever's desired
   while( fabs(curTerm) > eps)
   {
        curTerm *= x / (double)i;
        y += curTerm;
        ++i;
   }

通过这种方式,您永远不必明确计算因子问题,这些因子会增长得太快而无法用于此问题。

答案 3 :(得分:1)

这当然与数据结构有关。数据结构很有趣。其中一些对于较小的数据大小表现良好,而一些对较大的数据大小表现更好。

在递归代码中,有一个调用堆栈,当前递归的全部内容被推送到堆栈并在返回的路上被提取。这是每次递归调用的函数调用的额外开销。这就是原因,表现很慢。

详情请见http://publib.boulder.ibm.com/infocenter/iadthelp/v6r0/topic/com.ibm.etools.iseries.pgmgd.doc/c0925076137.htm

答案 4 :(得分:0)

函数调用在时间和空间上花费更多,因为:

  • 需要将参数推送到堆栈并弹出返回值。这需要时间。
  • 每次调用都会使用自己的堆栈“框架”。
    • 这不仅可以防止您进行非常深的递归(堆栈大小有限,通常为几MB),
    • 它也会伤害你的缓存局部性(因为你每次调用时都会遇到RAM的不同部分)并最终花费时间。

BTW,当你说函数调用“做的操作较少”时,这实际上是不真实的。函数调用在源代码中可能看起来较短,但外部的外观与内部实际的内容

之间存在差异。

此外,虽然在这种情况下不相关,但“较少的操作”并不总是等于更好的性能。有时,“更多操作”但具有更好的位置可以更好地利用所有现代CPU实现的缓存和预取来隐藏RAM延迟。