11

我正在实现一种算法来计算 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...
4

5 回答 5

12

浮点比较是精确的,因此10^-100.0.

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

while(fabs(tmp) > 10e-7)
于 2011-02-01T03:21:57.547 回答
2

处理浮点数时不要使用精确相等运算。尽管您的号码可能看起来0,但很可能是这样的0.00000000000000000000001

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

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

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

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

当然,如果您是数学家,那么任何误差都不够小:-)

于 2011-02-01T03:21:46.163 回答
0

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

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

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

while(fabs(tmp) >= 0.0001)

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

于 2011-02-01T03:22:37.097 回答
0

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

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

于 2011-02-01T03:30:13.573 回答
0

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

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 值并没有相互求和,因此没有累积,除非每个实际影响总和 - 因此上述方法在不进一步影响精度的情况下有效。

于 2011-02-01T03:56:00.787 回答