【问题标题】:Why is there a rounding difference between my normal recursion and tail recursion example?为什么我的正常递归和尾递归示例之间存在舍入差异?
【发布时间】:2012-08-07 00:58:42
【问题描述】:

在玩尾递归示例时,我注意到正常递归调用和尾递归调用的结果之间存在细微差异:

scala> def fact(n: Int): Double = if(n < 1) 1 else n * fact(n - 1)
fact: (n: Int)Double

scala> fact(30)
res31: Double = 2.6525285981219103E32

scala> @tailrec def fact(n: Int, acc: Double = 1): Double = if(n < 1) acc else fact(n - 1, n * acc)
fact: (n: Int, acc: Double)Double

scala> fact(30)
res32: Double = 2.652528598121911E32

出于好奇,有人可以向我解释一下为什么或在哪里进行舍入。我的猜测是,因为 Scala 编译器将尾递归版本转换为循环,所以在循环的每次迭代中都会分配 acc 参数,并且小的舍入误差会滑入其中。

【问题讨论】:

  • 在适当的编程语言(如 Scala)中,将 Double 结果分配给 Double 变量不会引入舍入错误。

标签: scala


【解决方案1】:

结果不同,因为两个版本做乘法的顺序不同,从而导致舍入不同。

正常的递归调用导致表达式n*([n-1]*([n-2]*(...))),因为您首先计算fact(n-1) 的值,然后将其与n 相乘,而尾递归调用导致((n*[n-1])*[n-2])*...,因为您首先乘以n 然后迭代到 n-1。

尝试重写其中一个版本,使其以另一种方式迭代,理论上你应该得到相同的答案。

【讨论】:

    【解决方案2】:

    您的两个函数的操作顺序不同。

    在 C 中:

    int main(int c, char **v)
    {
      printf ("%.16e %.16e\n", 
          30.*29*28*27*26*25*24*23*22*21*20*19*18*17*16*15*14*13*12*11*10*9*8*7*6*5*4*3*2,
          2.*3*4*5*6*7*8*9*10*11*12*13*14*15*16*17*18*19*20*21*22*23*24*25*26*27*28*29*30);
    }
    

    打印:

    2.6525285981219110e+32 2.6525285981219103e+32
    

    (我使用了一个平台,C 中的浮点可以在其上工作)

    您的函数的一个版本计算30.*29*...,另一个版本计算2.*3*...。 这两个结果略有不同是正常的:浮点运算不是关联的。但请注意,结果没有什么深不可测的。您的一个函数精确计算 IEEE 754 双精度表达式30.*29*...,另一个计算精确 2.*3*...。它们都按设计工作。

    如果非要猜的话,我希望2.*3*... 更准确(更接近用实数得到的结果),但没关系:这两个数字非常接近,也非常接近真实结果。

    【讨论】:

    • +1。奇怪的事情,但我只是在 C# 中尝试过同样的事情。两种实现都返回完全相同的结果。
    • 感谢您与 C 的出色比较。我赞成您的回答,但将新人标记为正确,因为他的代表较少并且也是正确的。希望没问题。
    • @JacobusR 好主意。与 C 相比,真正的原因是我手头没有 Scala 编译器,但如果它有助于消除浮点不可预测性的神话,那就更好了:)
    【解决方案3】:

    不同之处不在于 Scala 将尾递归变成了循环。如果没有优化,结果将是相同的。此外,在舍入误差方面,递归的作用与循环的作用没有什么不同。

    不同之处在于数字相乘的顺序。您的第一个解决方案在开始乘以数字之前一直递归到 1。所以它最终会计算n * ( (n - 1) * (... * (2 * 1)))。尾递归版本立即开始相乘,因此最终计算出n * (n-1) * ... * 2 * 1

    当然,通常我们会说这两个是相同的,因为乘法是关联的,但浮点运算并非如此。使用浮点 (x * y) * z 很可能与 x * (y * z) 不同,因为舍入误差的传播方式不同。这解释了你的行为。

    请注意,使用从 1 到 n 计数的 for 循环与从 n 到 1 计数的 for 循环来实现阶乘时,您会看到相同的差异。

    【讨论】:

      猜你喜欢
      • 2016-10-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-14
      • 1970-01-01
      • 2011-04-10
      • 1970-01-01
      相关资源
      最近更新 更多