确定是否可以使用递归解决问题的考虑因素是什么?


10

有时在面试中,我可能会使用递归来解决问题(例如,添加1到无限精度整数),或者当问题本身适合使用递归时。有时,可能是由于大量使用递归来解决问题,因此,不用考虑太多,就使用递归来解决问题。

但是,在决定使用递归解决问题之前,应考虑哪些因素?


我有一些想法:

如果我们对每次都减半的数据使用递归,那么使用递归似乎没有问题,因为所有可以容纳16GB RAM甚至8TB硬盘的数据都可以通过深度仅为42层的递归进行处理。(因此不会出现堆栈溢出(我认为在某些环境中,堆栈的深度可以达到4000层,远超过42级,但是同时,这还取决于您拥有多少局部变量,因为每个调用堆栈占用的内存更多)如果有很多局部变量,则是确定堆栈溢出的是内存大小而不是级别)。

如果您使用纯递归来计算斐波那契数,那么您真的要担心时间复杂性,除非您缓存中间结果。

以及如何添加1到无限精度整数?也许这是有争议的,例如,您将使用长度为3000位数或4000位数的数字,以至于它会导致堆栈溢出吗?我没想到,但是答案可能不是,我们不应该使用递归,而应该使用普通循环,因为如果在某些应用程序中,该数字确实需要4000位数长,以检查某些数字,该怎么办?数字的属性,例如数字是否为质数。

最终的问题是:在决定使用递归解决问题之前,有哪些注意事项?


7
它实际上很简单:“如果我能假设已知一个较小问题的解决方案,该解决方案是否简单?”
Kilian Foth,

但是斐波那契数或加到1无限精度整数呢?您可以说,是的,它们可以简化为较小的问题,但是纯递归不适合此操作
极性

您可能会发现这是很有帮助的- stackoverflow.com/questions/3021/...
基肖尔Kundan

Answers:


15

一个考虑因素是您的算法是打算用作抽象解决方案还是实用的可执行解决方案。在前一种情况下,您要寻找的属性是正确性,并且易于目标受众理解1。在后一种情况下,性能也是一个问题。这些注意事项可能会影响您的选择。

第二个考虑因素(针对实际解决方案)是您使用的编程语言(或更严格地说是其实现)是否消除了尾部调用?如果没有消除尾部调用,则递归比迭代慢,并且深度递归可能导致堆栈溢出问题。

请注意,(正确的)递归解决方案可以转换为等效的非递归解决方案,因此您不必在这两种方法之间做出艰难的选择。

最后,有时在递归公式和非递归公式之间进行选择是出于需要证明(在形式上)算法的性质的需要。递归公式更直接地允许归纳证明。


1-这包括诸如目标受众是否这样的考虑……并且这可能包括阅读实用代码的程序员……会认为一种解决方案样式比另一种更为“自然”。“自然”的概念因人而异,这取决于他们学习编程或算法的方式。(我向提出“自然”作为决定使用递归(或不使用)以客观术语定义“自然” 的主要标准的主要标准的人提出挑战;即您将如何测量它)。


2
使用递归可以更自然地表达一些问题。例如,树遍历。
Frank Hileman '17

更新了我的答案以解决这一点,
Stephen C

1
关于“自然性”:例如,不进行递归的树遍历往往会产生较大的通用代码。考虑例如使用多态调用遍历树,叶节点和复合节点的行为不同。没有递归,这是不可能的。
Frank Hileman

1)您是否接受了定义“自然”的挑战?2)由于可以使用堆栈数据结构模拟递归,因此也可以以这种方式实现树遍历。这可能不是最有效的方法……并且它不会为您提供最易读的代码……但是这样做绝对是可行且实用的。
斯蒂芬C,

顺便说一下,我学到的第一种编程语言(FORTRAN 4)根本不支持递归。
斯蒂芬C,

1

作为C / C ++程序员,我首要考虑的是性能。我的决策过程类似于:

  1. 调用堆栈的最大深度是多少?如果太深,请摆脱递归。如果浅,请执行2。

  2. 此功能可能是我程序的瓶颈吗?如果是,请转到3。如果否,请保留递归。如果不确定,请运行分析器。

  3. 递归函数调用所花费的CPU时间的百分比是多少?如果函数调用比其他函数体花费的时间少得多,则可以使用递归。


0

但是,在决定使用递归解决问题之前,应考虑哪些因素?

在Scheme中编写函数时,我发现写尾递归函数很自然,而不必考虑太多。

当用C ++编写函数时,在使用递归函数之前,我发现自己在争论。我问自己的问题是:

  • 可以使用迭代算法执行计算吗?如果是,请使用迭代方法。

  • 递归的深度能否随模型的大小而增长?我最近遇到了一个案例,其中由于模型的大小,递归的深度增加到将近13000。我不得不将函数转换为在事后使用迭代算法。

    因此,我不建议使用递归函数编写树遍历算法。您永远不知道树何时对于您的运行时环境变得太深。

  • 通过使用迭代算法,函数是否会变得太复杂?如果是,请使用递归函数。我没有尝试qsort使用迭代方法进行编写,但是我觉得使用递归函数更自然。


0

对于斐波那契数,幼稚的“递归”完全是愚蠢的。那是因为它导致相同的子问题一遍又一遍地被解决。

实际上,斐波那契数有一个很小的变化,其中递归非常有效:给定一个数n≥1,同时计算fib(n)和fib(n-1)。因此,您需要一个返回两个结果的函数,让我们将此函数称为fib2。

实现非常简单:

function fib2 (n) -> (fibn, fibnm1) {
    if n ≤ 1 { return (1, 1) }
    let (fibn, fibnm1) = fib2 (n-1)
    return (fibn + fibnm1, fibn)
}

您认为您可以用通用语言编写程序吗?并且您fib2返回一对数字,而您fib2()的接口不适合fib(),即给定一个数字,则返回一个数字。看来您fib(n)要回来了,fib2(n)[0]但请具体说明
极性
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.