【问题标题】:Are recursive functions used in R?R中是否使用递归函数?
【发布时间】:2011-03-11 14:03:00
【问题描述】:

演示递归的规范函数是 factorial() 函数。我自己尝试了一个简单的实现并想出了这个:

factorial <- function(x){

if(x==1)
    return( 1)
else
    return(x*factorial(x-1))

}

从我对该主题的调查来看,似乎存在一些关于使用递归还是简单迭代更好的争论。我想看看 R 是如何实现它的,并在 gregmisc 包中找到了一个 factorial() 函数。我以为我会找到类似我的实现的东西,或者是定期迭代。我发现了什么:

> factorial
function (x) 
gamma(x + 1)
<environment: namespace:base>

因此,对于我关于 R 更喜欢递归还是迭代的问题的答案是“都不喜欢”。至少在这个实现中。在 R 中是否有充分的理由避免使用递归函数?

更新:

gregmisc 版本:

>ptm <- proc.time()
> factorial(144)
[1] 5.550294e+249
> proc.time() - ptm
   user  system elapsed 
  0.001   0.000   0.001 

我的版本:

> factorial(144)
[1] 5.550294e+249
> proc.time() - ptm
  user  system elapsed 
  0.002   0.001   0.006 

【问题讨论】:

  • 还有 prod(2:144) 虽然我猜这可能会更慢。
  • @Tony 它实际上比其他两个更快。
  • 浮点版本对于小整数肯定会很慢。

标签: r recursion


【解决方案1】:

对于整数阶乘的计算,递归实现更慢更复杂。在生产代码中总是使用迭代。

您所指的factorial 函数在基础包中。它对实数值而不是整数进行操作,因此实现。其文档指出:

factorial(x) (x! 表示非负数 整数 x) 被定义为 gamma(x+1)

一个更有趣的例子是实现斐波那契数列的代码,当使用朴素的递归实现时非常浪费。递归方法可以通过记忆提高效率,但如果性能受到威胁,则始终首选简单的迭代。

另一种以递归方式自然表达的常用算法是快速排序。就像所有算法一样,这可以在没有递归的情况下实现,但是这样做非常复杂。使用非递归快速排序几乎没有什么好处,因此使用朴素递归实现很常见。

递归是一个不错的实现选择:

  • 如果性能不受影响,并且
  • 如果递归实现更自然(因此更容易验证和维护)。

【讨论】:

  • 这些指南是否特别适用于 R?或者换一种说法,你会说不管是 C++ 还是 R,这些准则都适用于任何语言?
  • 请注意,这些建议对于实现尾调用消除会有些不同,它(有效地)是某些类型的递归到迭代的自动转换。
  • @hadley 我认为不同的语言能力会影响你选择实现算法的方式,但我仍然认为你会遵循我的两个要点中表达的原则。只是,如果您有诸如尾调用消除之类的东西,那么第一个要点就不会成为问题。
  • @David Coudl 您提供任何关于哪些不是简单递归函数的参考(可以在堆栈或任何地方的网站)?
  • @manoel 通常天真的简单算法是递归算法,而棘手的是非递归变体。
【解决方案2】:

我认为在 R 中表达数学计算最自然的方式是通过数学/统计符号。该语言的全部意义在于使以自然方式表达统计计算变得容易。

您拥有使用gamma 实现的factorial 示例非常适合此视图。我不知道gamma 是如何实现的,但我们不需要知道这一点才能使用R。作为用户,最重要的通常是得到正确的答案。如果代码被证明非常慢,那就是你优化的时候。首先要开始的是数学和算法的选择,而不是实现细节。

David Heffernan 是正确的,递归通常比迭代慢,但他似乎没有考虑到它并不重要。使用他自己的例子,斐波那契数,真正重要的是避免重新计算数字,这可以通过记忆来完成。这使得计算以线性时间而不是指数时间运行 - 一个巨大的改进。完成此操作后,您仍然可以通过使用循环实现算法来获得微小的改进,但这可能并不重要。 此外,还有一个closed form

阶乘函数和斐波那契数也增长得非常快。这意味着每个算术运算(加法、乘法等)都将开始花费很长时间,而递归不会变得更昂贵或至少不会那么快。同样,数学考虑胜过实现细节。

TL;DR

我的建议是:

  1. 以最简单的方式编写算法。
  2. 确保它是正确的。
  3. 如果且仅当算法在实际输入上太慢:
    1. 找出算法的哪一部分花费时间/时间复杂度是多少。
    2. 修复那个部分并且只修复那个部分。
    3. 如有必要,重复该过程。

【讨论】:

    【解决方案3】:

    答案是肯定的,R 中使用了递归函数。虽然大部分 R 本身是用 R 编写的,但一些高度优化的例程是 C 或 FORTRAN 的包装器。此外,R-BASE 的大部分内容都是原始的。基本上,最有可能使用递归的快速例程最不可能被看到,除非有人真正查看了二进制文件。

    递归函数的一个很好的例子可能是最常用的函数:

    > c
    function (..., recursive = FALSE)  .Primitive("c")
    
    > set.seed(1)
    > x <- list('a'=list(1:2, list(rnorm(10)), 'b'=rnorm(3)))
    > c(x)
    $a
    $a[[1]]
    [1] 1 2
    
    $a[[2]]
    $a[[2]][[1]]
     [1] -0.6264538  0.1836433 -0.8356286  1.5952808  0.3295078 -0.8204684  0.4874291  0.7383247
     [9]  0.5757814 -0.3053884
    
    
    $a$b
    [1]  1.5117812  0.3898432 -0.6212406
    
    
    > c(x, recursive=TRUE)
            a1         a2         a3         a4         a5         a6         a7         a8 
     1.0000000  2.0000000 -0.6264538  0.1836433 -0.8356286  1.5952808  0.3295078 -0.8204684 
            a9        a10        a11        a12       a.b1       a.b2       a.b3 
     0.4874291  0.7383247  0.5757814 -0.3053884  1.5117812  0.3898432 -0.6212406 
    > 
    

    【讨论】:

    • 这不是 OP 所指的递归,它只是意味着该函数通过扁平化它们应用于其参数中的嵌套列表(而不是仅仅遍历列表的顶层)。当然,这可能是通过递归实现的,但不一定,而且参数名称并不意味着如此。请注意,文档没有具体说明这是如何实现的——在这种情况下,“通过列表递归下降”不是实现提示,而是类比描述。
    • @KonradRudolph 感谢您的澄清。鉴于 R 是如何如此——如果我在这里错了——请原谅我——强类型 (??),如果复杂嵌套的列表只是长的值序列,并带有每个值的嵌套级别指示符,我不会感到惊讶。
    猜你喜欢
    • 2020-11-05
    • 2015-09-12
    • 2013-04-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-12
    • 1970-01-01
    • 2015-07-03
    相关资源
    最近更新 更多