Двойной равен 0 проблема в C - PullRequest
9 голосов
/ 01 февраля 2011

Я реализовывал алгоритм для вычисления натуральных бревен в 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...

Ответы [ 5 ]

10 голосов
/ 01 февраля 2011

Сравнение с плавающей точкой является точным, поэтому 10^-10 отличается от 0.0.

По сути, вы должны сравнивать с некоторой допустимой разницей, скажем 10^-7, основанной на количестве записанных десятичных знаков, что может быть достигнуто как:

while(fabs(tmp) > 10e-7)
2 голосов
/ 01 февраля 2011

Не используйте операции точного равенства при работе с числами с плавающей запятой.Хотя ваш номер может выглядеть как 0, он, скорее всего, будет выглядеть как 0.00000000000000000000001.

. Вы увидите это, если будете использовать %.50f вместо %f в своемформат строкиПоследний использует разумное значение по умолчанию для десятичных разрядов (6 в вашем случае), но первый явно заявляет, что вы хотите много.

Для безопасности используйте дельту, чтобы проверить, достаточно ли она близка, например:

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

Очевидно, что дельта полностью зависит от ваших потребностей.Если вы говорите о деньгах, 10 -5 может быть достаточно.Если вы физик, вам, вероятно, следует выбрать меньшее значение.

Конечно, если вы математик, нет достаточно малой погрешности: -)

0 голосов
/ 01 февраля 2011

Много обсуждений причины, но вот альтернативное решение:

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 в любом случае не суммировались друг с другом, поэтому нет накопления, если каждое из них не влияет на сумму - следовательно, описанный выше подход работает без дальнейшей потери точности.

0 голосов
/ 01 февраля 2011

Оператор печати отображает округленное значение, он не печатает с максимально возможной точностью. Таким образом, ваш цикл еще не достиг нуля.

(И, как уже упоминалось, из-за проблем округления он может фактически никогда не достичь его. Поэтому сравнение значения с небольшим пределом является более надежным, чем сравнение на равенство с 0,0.)

0 голосов
/ 01 февраля 2011

То, что число отображается как «0,000000», не означает, что оно равно 0,0. Десятичное отображение чисел имеет меньшую точность, чем может хранить двойное число.

Возможно, ваш алгоритм достигает точки, где он очень близок к 0, но следующий шаг перемещается так мало, что он округляется до того же значения, что и раньше, и, следовательно, он никогда не приближается к 0 ( просто заходит в бесконечный цикл).

В общем случае не следует сравнивать числа с плавающей запятой с == и !=. Вы должны всегда проверять, находятся ли они в определенном небольшом диапазоне (обычно это называется эпсилон). Например:

while(fabs(tmp) >= 0.0001)

Затем он остановится, когда приблизится к 0.

...