【问题标题】:Double equals 0 problem in CC中的双等于0问题
【发布时间】:2011-06-19 00:21:56
【问题描述】:

我正在实现一个算法来计算 C 中的自然对数。

double taylor_ln(int z) {
    double sum = 0.0;
    double tmp = 1.0;

    int i = 1;
    while(tmp != 0.0) {
        tmp = (1.0 / i) * (pow(((z - 1.0) / (z + 1.0)), i));
        printf("(1.0 / %d) * (pow(((%d - 1.0) / (%d + 1.0)), %d)) = %f\n", i, z, z, i, tmp);
        sum += tmp;
        i += 2;
    }

    return sum * 2;
}

如 print 语句所示,tmp 最终确实等于 0.0,但是,循环继续。这可能是什么原因造成的?

我在 Fedora 14 amd64 上编译:

clang -lm -o taylor_ln taylor_ln.c

例子:

$ ./taylor_ln 2
(1.0 / 1) * (pow(((2 - 1.0) / (2 + 1.0)), 1)) = 0.333333
(1.0 / 3) * (pow(((2 - 1.0) / (2 + 1.0)), 3)) = 0.012346
(1.0 / 5) * (pow(((2 - 1.0) / (2 + 1.0)), 5)) = 0.000823
(1.0 / 7) * (pow(((2 - 1.0) / (2 + 1.0)), 7)) = 0.000065
(1.0 / 9) * (pow(((2 - 1.0) / (2 + 1.0)), 9)) = 0.000006
(1.0 / 11) * (pow(((2 - 1.0) / (2 + 1.0)), 11)) = 0.000001
(1.0 / 13) * (pow(((2 - 1.0) / (2 + 1.0)), 13)) = 0.000000
(1.0 / 15) * (pow(((2 - 1.0) / (2 + 1.0)), 15)) = 0.000000
(1.0 / 17) * (pow(((2 - 1.0) / (2 + 1.0)), 17)) = 0.000000
(1.0 / 19) * (pow(((2 - 1.0) / (2 + 1.0)), 19)) = 0.000000
(1.0 / 21) * (pow(((2 - 1.0) / (2 + 1.0)), 21)) = 0.000000
and so on...

【问题讨论】:

标签: c floating-point logarithm


【解决方案1】:

浮点比较是精确的,所以10^-100.0不一样。

基本上,您应该根据您写出的小数位数与一些可容忍的差异进行比较,例如10^-7,可以通过以下方式完成:

while(fabs(tmp) > 10e-7)

【讨论】:

  • 两件事:abs是整数运算;使用fabs。并且您希望大于某个阈值,而不是小于。
  • @chrisaycock:你应该已经给出了答案......它(或简化版本)是这里大多数浮点相关问题的正确答案。
  • @Ben 每天都会在 SO 上提出此类问题。我认为任何高于一定声誉水平的人在他的 SO 职业生涯中至少发布过一次该链接。
  • @chrisaycock:我当然有。关于这个问题,真正让我感到惊讶的唯一一点是,它还没有作为副本被关闭,提出了五个不同的问题作为“原始”。
【解决方案2】:

在处理浮点数时不要使用精确的相等运算。尽管您的电话号码可能看起来类似于0,但它可能类似于0.00000000000000000000001

如果您在格式字符串中使用%.50f 而不是%f,您会看到这一点。后者对小数位使用合理的默认值(在您的情况下为 6),但前者明确声明您需要很多。

为了安全起见,使用delta来检查是否足够接近,例如:

if (fabs (val) < 0.0001) {
    // close enough.
}

显然,增量完全取决于您的需求。如果你说的是钱,10-5 可能就足够了。如果您是物理学家,您可能应该选择较小的值。

当然,如果你是一名数学家,任何误差都不够小:-)

【讨论】:

    【解决方案3】:

    仅仅因为数字显示为“0.000000”并不意味着它等于 0.0。数字的十进制显示的精度低于 double 可以存储的精度。

    您的算法可能会达到非常接近 0 的点,但下一步移动的幅度很小,以至于它会四舍五入到与之前相同的值,因此它永远不会更接近 0 (只是进入一个无限循环)。

    一般来说,您不应将浮点数与==!= 进行比较。您应该始终检查它们是否在某个小范围内(通常称为 epsilon)。例如:

    while(fabs(tmp) >= 0.0001)
    

    然后它会在合理接近 0 时停止。

    【讨论】:

      【解决方案4】:

      打印语句显示的是一个四舍五入的值,它没有打印尽可能高的精度。所以你的循环还没有真正达到零。

      (而且,正如其他人所提到的,由于舍入问题,它实际上可能永远无法达到它。因此,将值与小限制进行比较比将相等性与 0.0 进行比较更可靠。)

      【讨论】:

        【解决方案5】:

        对原因进行了大量讨论,但这里有一个替代解决方案:

        double taylor_ln(int z)
        {
            double sum = 0.0;
            double tmp, old_sum;
            int i = 1;
            do 
            {
                old_sum = sum;
                tmp = (1.0 / i) * (pow(((z - 1.0) / (z + 1.0)), i));
                printf("(1.0 / %d) * (pow(((%d - 1.0) / (%d + 1.0)), %d)) = %f\n",
                       i, z, z, i, tmp);
                sum += tmp;
                i += 2;
            } while (sum != old_sum);
            return sum * 2;
         }
        

        这种方法侧重于 tmp 的每个递减值是否会对总和产生明显的差异。这比从 0 开始计算某个阈值更容易,在该阈值处 tmp 变得微不足道,并且可能在不改变结果的情况下提前终止。

        请注意,当您将一个相对较大的数字与一个相对较小的数字相加时,结果中的有效数字会限制精度。相比之下,如果你把几个小的相加然后把它加到大的上,你可能有足够的钱把大的加起来一点。在您的算法中,小 tmp 值并没有相互求和,因此没有累积,除非每个实际影响总和 - 因此上述方法在不进一步影响精度的情况下有效。

        【讨论】:

          猜你喜欢
          • 2014-11-22
          • 2011-03-09
          • 2014-05-13
          • 1970-01-01
          • 1970-01-01
          • 2013-12-15
          • 1970-01-01
          • 1970-01-01
          • 2017-06-06
          相关资源
          最近更新 更多