朴素递归
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 术语中,这是一种在指数时间中运行的算法。用更通俗的话来说,这个算法真的很慢。
想想这里发生了什么:
函数调用的数量随着输入的大小呈指数增长。哎哟!
不仅算法的运行时间会受到影响,空间复杂度也会受到影响。具有讽刺意味的是,您在使用朴素递归时可能遇到的问题被称为堆栈溢出,其中函数调用堆栈溢出 有大量的函数调用并且可用的堆栈空间基本上用完了。双重哎哟!
不仅该函数的时间和空间复杂度随着输入呈指数增长,而且该算法还非常清楚地以的方式完成了超出其需要的工作。每次执行f(n) 并且基本情况没有命中时会发生什么? f(n - 1) 被计算,两次。三重哎哟!
所以,很明显这个算法很糟糕。但是有什么办法呢?
通用子表达式消除
一种长期加速程序运行时间的优化被称为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)) 直接递归版本。
很酷吧?
附录:动态规划
虽然您的具体示例不需要像我最初认为的那样需要动态编程,但在谈论递归时,这仍然是一个非常有用的话题,因此我对这部分进行了重新设计,使其不那么做作以前是。此外,这部分是附录,因为我将在下面使用c++ 语法。如果这激怒了任何人,我深表歉意,我只是不喜欢目前重新实现c++ 的std::map 的想法(也许在将来......)。
也许你听说过dynamic programming。不,请不要畏缩!听起来很吓人,但事实并非如此。其实还是很厉害的!
非常简单地说,动态编程是一种智能的蛮力方法。这个想法是您memoize将先前计算的结果放入一个查找表中,以便如果您需要重新计算某些东西(并且使用某些算法,您正在做很多很多),答案只是一个恒定时间(O(1)!)查找。
我们以Fibonacci sequence 为例。斐波那契算法的标准、幼稚、普通的实现如下所示:
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];
}
添加一个查找表(实现为c++ std::map<int, long long>),稍微调整一下逻辑,然后将普通的int 值替换为long long 值,然后你就得到了一个版本斐波那契算法可以处理更大的n 值,快得多。说真的,自己尝试并比较。天真的算法可能需要 几小时(或几天,或更长时间)才能完成,而动态编程版本可以在 秒 内完成。
所以...我希望所有这些都回答了您的问题(也许更多)。让我知道你是否还有其他人! :)
跟进: 只是为了确切地说明您的未简化表达式的效率有多低 - 就在我第一次提交这个问题的时候,我运行了这个程序的两个版本(简化版和天真的递归版本)在n = 50 的输入上背靠背。我的台式机包括一个 Intel i7-4770K,相关进程目前正在使用我 CPU 处理能力的 13% 左右。快速动态编程版本在几秒钟内完成,输出为1125899906842624。近 12 小时后,当我打字时,朴素的递归版本仍在工作。我想它的工作时间会更长(如果我允许的话!)。
感谢 Jim Balter 的所有更正,让我意识到动态编程是有用的,但在这里完全没有必要!像往常一样,我让事情变得比需要的复杂得多。 OP 不是今天在这里学习新东西的唯一人! :)