关于递归的一般问题

时间:2011-08-01 21:19:50

标签: language-agnostic recursion performance

据我了解,良好的递归解决方案可以使复杂的问题变得更容易。它们在时间或空间方面都更有效率。

我的问题是:这不是免费的,调用堆栈会非常深。它会占用大量内存。我是对的吗?

6 个答案:

答案 0 :(得分:12)

很难准确确定递归所涉及的权衡。

在数学上抽象的层次上,递归提供了一个强大的框架来描述函数的显式行为。例如,我们可以在数学上将阶乘定义为

x! = 1             if x == 0
x! = x * (x - 1)!  else

或者我们可以递归地定义一个更复杂的函数,例如我们如何计算“N choose K”:

C(n, k) = 1                             if k == 0
C(n, k) = 0                             if k < 0 or if n > k
C(n, k) = C(n - 1, k) + C(n - 1, k - 1) else

使用递归作为实现技术时,无法保证最终会使用更多内存或生成运行效率更高的代码。通常,由于保存堆栈帧所需的内存,递归使用更多空间,但在某些语言中,这不是问题,因为编译器可以尝试优化函数调用(例如,参见尾调用消除)。在其他情况下,递归会占用大量资源,导致递归代码无法在简单问题上终止。

至于效率问题,通常递归代码的效率远低于迭代代码。函数调用很昂贵,从递归到代码的简单转换会导致不必要的重复工作。例如,天真的斐波纳契实现

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

极其低效且速度太慢,从未在实践中使用过。虽然代码更清晰,但效率低下会消除递归的任何潜在好处。

在其他情况下,递归可以节省大量时间。例如,mergesort是一种非常快速的排序算法,由一个漂亮的递归定义:

Mergesort(array, low, high) {
    if (low >= high - 1) return;
    Mergesort(array, low, low + (high - low) / 2);
    Mergesort(array, low + (high - low) / 2, high);
    Merge(array, low, low + (high - low) / 2, high);
}

此代码非常快,相应的迭代代码可能会更慢,更难阅读,也更难理解。

因此,简而言之,递归既不是一种神奇的治疗方式,也不是一种可以避免的力量。它有助于阐明许多问题的结构,否则这些问题似乎很难或几乎不可能。虽然它通常会导致更清晰的代码,但它通常以牺牲时间和内存为代价(尽管它不一定自动效率较低;在许多情况下它可能更有效)。即使你从未在生活中写过另一个递归函数,也绝对值得研究,以提高你的整体算法思维和解决问题的能力。

希望这有帮助!

答案 1 :(得分:1)

实际上调用堆栈不会很深。例如,像quicksort这样的分而治之算法将问题分成两半。使用深度为32的调用堆栈,您可以对4G元素进行排序,这可能甚至不适合普通计算机的内存。内存消耗并不是真正的问题,它只是一个堆栈而且它是免费的,只要你没有用完它...(并且有32个级别你需要为每个级别存储大量数据)。

如果在堆栈结构中维护上的状态,则可以将几乎所有的resursive进程重写为迭代进程,但这只会使代码复杂化。您可以从重写中获得实际好处的主要方案是,您有一个尾递归代码,不需要为每个递归调用维护状态。请注意,对于某些语言(大多数函数式编程语言和C / C ++,也许是Java),一个好的编译器可以为你做到这一点。

答案 2 :(得分:1)

这取决于。递归最适合的问题将抵抗这个问题。一个常见的例子是Mergesort,其中对于N个项目的列表进行排序将有关于log2(N)堆栈帧。因此,如果您的堆栈帧限制为200,并且您在调用Mergesort时使用了50,那么仍然足以排序大约2 ^ 150个项目而没有堆栈溢出。此外,Mergesort不会为每个堆栈帧创建大量内存,因此Mergesort的总内存使用量不应超过原始列表的两倍。

此外,一些语言(Scheme是一个很好的例子)使用tail-call elimination,以便可以使用递归优雅地编写代码,但随后优化或编译成迭代循环。这是作为函数式语言的LISP在执行速度方面仍然能够与C和C ++竞争的方式之一。

还有另一种称为Trampolining的技术可用于执行看似递归的操作,而不会产生深度调用堆栈。但除非它被构建到库或甚至语言级构造中,否则这种技术在生产力方面具有不太明显的优势(在我看来)。

因此,虽然在很多情况下很难反对一个好的旧for x in xrange(10)循环,但递归确实有它的位置。

答案 3 :(得分:0)

Cons:
 It is hard (especially for inexperienced programmers) to think recursively
 There also exists the problem of stack overflow when using some forms of recursion (head
recursion).
 It is usually less efficient because of having to push and pop recursions on and off the
run-time stack, so it can be slower to run than simple iteration.

但为什么我们懒得使用递归?

Pros:
 It is easier to code a recursive solution once one is able to identify that solution. The
recursive code is usually smaller, more concise, more elegant, and possibly even easier to
understand, though that depends on one’s thinking style☺
 There are some problems that are very difficult to solve without recursion. Those problems
that require backtracking such as searching a maze for a path to an exit or tree based
operations are best solved recursively.

答案 4 :(得分:0)

如果你的递归不是尾递归而且你的语言不支持尾递归,那么这只是很昂贵的。请参阅以下关于尾部调用的维基百科文章,以讨论该主题:

http://en.wikipedia.org/wiki/Tail_call

否则,它可以使代码更容易阅读,更容易测试。

答案 5 :(得分:0)

这取决于问题。

如果问题需要递归,就像深度优先树步行一样,唯一可以避免递归的方法是通过编写自己的堆栈来模拟它。这不会保存任何东西。

如果问题不需要递归,就像通常的ho-hum factorial或fibonacci函数一样,重点是什么?你没有通过使用它获得任何东西。

这是一个非常小的问题,你甚至可能有合理的选择。