大数字的递归函数背后究竟发生了什么?

时间:2014-08-09 17:52:28

标签: c recursion computer-science

我在下面有一个递归函数。

int f(int n){
  if(n<1) return 1;
  else return f(n-1) + f(n-1);
}

当我用f(0),f(1)等小数字调用函数时,它工作正常。

但是当我拨打 f(50) f(80) f(100)时,它只是等待&amp;没有显示输出。

我需要知道背后究竟发生了什么?

4 个答案:

答案 0 :(得分:9)

朴素递归

维基百科定义的

Recursion

  

递归是以自相似的方式重复项目的过程。

您的计划正在解决数学recurrence relation

f(n) = f(n - 1) + f(n - 1)

通过调用自身,将f(n)的较大问题分解为越来越小的块,然后将这些块分成越来越小的块,依此类推。

致电f(0)时发生了什么?因为在这种情况下参数n为零,所以你的基本情况被触发并且递归链停止。这是非常简单的情况(就像任何n < 1):

    f(0)
     |
     1

f(1)怎么样?有点复杂但不多:

    f(1)
  /     \
f(0) +  f(0) = 1 + 1 = 2

让我们尝试一些更大的东西,比如n = 5

             _____________f(5)___________
            /                            \
        ___f(4)____        +        ____f(4)____
       /           \               /            \
    f(3)    +     f(3)     +     f(3)     +    f(3)
   /   \         /   \          /    \        /    \
f(2) + f(2) + f(2) + f(2)  +  f(2) + f(2) + f(2) + f(2)
/ \    / \     / \    / \      / \    / \    / \    / \
...    ...     ...    ...      ...    ...    ...    ... = f(0) * 32 = 1 * 32 = 32

...所以,事实证明,手工创建文本树非常烦人。希望你现在能够获得这个想法。也许,你甚至已经发现了这种模式:

f(0) = 1
f(1) = 2
f(2) = 4
f(3) = 8
f(4) = 16
f(5) = 32
...

一般而言:

f(n) = 2ⁿ

从数学上讲,这是一个指数方程。在Big-O术语中,这是一种在指数时间中运行的算法。用更通俗的术语来说,这个算法真是太神了。

想想这里发生了什么:

  1. 正在进行的函数调用次数随输入的大小呈指数增长。哎哟!

  2. 不仅算法的运行时间受到影响, space 的复杂性也会受到影响。具有讽刺意味的是,您可能通过朴素递归遇到的问题被称为堆栈溢出,其中函数调用堆栈溢出且数量巨大函数调用和自由堆栈空间基本上用完了。 Double ouch!

  3. 此功能的时间和空间复杂性不仅随着输入呈指数级增长,而且算法也非常明显地执行方式更多的工作。每次执行f(n)并且基本案例时,会发生什么?计算f(n - 1)两次。三重!

  4. 所以,很明显这个算法糟透了。但是可以做些什么呢?

    常见的表达消除

    一种以方式加速程序运行时间的优化称为common subexpression elimination。这是一个非常快速和简单的优化实现,它消除了天真版本所做的绝大多数函数调用。你需要做的就是意识到这一点:

    return f(n - 1) + f(n - 1);
    

    相当于:

    return 2 * f(n - 1);
    

    以便您的代码成为:

    int f(int n)
    {
        if(n < 1)
        {
            return 1;
        }
    
        else
        {
            return 2 * f(n-1);
        }
    }
    

    与原始版本并排运行此修订版,并被运行时间之间的数量级差异所震撼!因为每次调用只进行一次递归调用,所以指数算法基本上变成等效迭代方法的线性时间(O(n))直接递归版本。

    非常酷,呵呵?

    附录:动态编程

    虽然您的具体示例并不像我原先认为的那样需要动态编程,但在谈论递归时,这仍然是一个非常有用的话题,因此我将此部分重新设计为比以前更少做作。此外,这是部分补遗,因为我将在下面使用语法。如果这会弄乱任何羽毛,我道歉,我只是不喜欢现在重新实施 std::map的想法(也许将来......)

    也许您已经听说过dynamic programming。不,请不要畏缩!这听起来很可怕,但实际上并非如此。实际上,它非常棒!

    非常简单地说,动态编程是一种蛮力的智能方法。这个想法是你 memoize 以前计算过的结果到查找表中所以如果您需要重新计算某些东西(并且使用某些算法,您正在做很多)那么答案就是一个恒定时间(O(1) !)查找。

    我们以Fibonacci sequence为例。 Fibonacci算法的标准,天真,普通的实现如下所示:

    int fib(int n)
    {
        if (n <= 1)
        {
            return n;
        }
    
        return fib(n - 1) + fib(n - 2);
    }
    

    以上是另一种指数时间(O(2ⁿ))算法。但是,优化此算法并不像以前那么简单,因为fib(n - 1) + fib(n - 2)不能以完全相同的方式简化。然而,我们可以做的是添加一个数据结构,旨在允许我们的程序不断访问预先计算的结果,并利用它来避免冗余计算的 ton 。因此优化版本是:

    long long fib_dp(int n)
    {
        if (lookup.find(n) != lookup.end())
        {
            return lookup[n];
        }
    
        else if (n <= 1)
        {
            return n;
        }
    
        lookup[n] = fib_dp(n - 1) + fib_dp(n - 2);
        return lookup[n];
    }
    

    添加一个查找表(实现为 std::map<int, long long>),稍稍调整逻辑,并为int值换出普通的long long值,并且您已经拥有了斐波纳契算法的一个版本,可以更快地处理更大的n 值。说真的,亲自试试并比较一下。天真算法可能花费小时(或几天或更多)完成,动态编程版本可以在中爆发。

    所以...我希望所有这些都回答了你的问题(也许更多)。如果您有其他人,请告诉我! :)

    后续行动:为了准确地将你的非简化表达式效率降低到家 - 就在我第一次提交这个问题的时候,我运行了这个程序的两个版本(简化版本)和{na}递归版本)背靠背n = 50的输入。我的桌面包括Intel i7-4770K,相关流程目前使用的CPU占处理能力的13%左右。快速动态编程版本在几秒钟内完成,输出为1125899906842624。在接近十二个小时之后,天真的递归版仍在我打字时工作。我想它会工作更长时间(如果我允许的话!)。

    感谢Jim Balter的所有更正,让我意识到动态编程很有用,但这里完全没必要!像往常一样,我做的事情比他们需要的要复杂得多。 OP不是今天唯一一个在这里学习新东西的人! :)

答案 1 :(得分:2)

您正在通过电话C来做这件事,只需花费很长时间就可以使用50100这样的大数字。此外,您的代码会输出任何内容。

这会提高你的节目速度。

int f(int n)
{
if(n<1) return 1;
else return f(n-1) * 2;
}

由于x + xx * 2相同。

希望这有帮助!

答案 2 :(得分:1)

该函数实际上返回值2 ^ n。因此,在值较小的情况下,返回值很容易存在于整数变量中。但是当&#34; n&#34;变为大于31左右,整数返回类型无法返回值,因此它不显示任何输出。

答案 3 :(得分:0)

让我们看一下f(x)x = 2的{​​{1}}时会发生什么:

x = 30

我们可以看到,对于f(2) = f(1) + f(1) = f(0) + f(0) + f(0) + f(0) = 1+1+1+1 = 4; ,我们得到了一个相对较小的添加链。为了得到我们的结果,我们必须对函数进行7次评估。让我们看看当我们将x = 2设置为30时会发生什么:

x

我们看到我们得到了很长的加法链,我们必须评估函数(sum(n = 0到29)(2 ^ n))次。这么多电话都是让节目变得如此慢的原因。