floor() ведет себя странно

Мне нужна была функция для округления положительного двойного числа до ближайшего целого числа. скрывающийся Aorund я нашел этот очень элегантный способ

int x = floor(y + 0.5);

Я написал простую программу тестирования:

double a = 10.0;

for (int i = 0; i < 10; i++) {
  cout << a << "\t" << a + 0.5 << "\t" << floor(a + 0.5) << endl;
  a += 0.1;
}

но я получаю какой-то странный вывод

10      10.5    10
10.1    10.6    10
10.2    10.7    10
10.3    10.8    10
10.4    10.9    10
10.5    11      10 <--- should be 11!
10.6    11.1    11
10.7    11.2    11
10.8    11.3    11
10.9    11.4    11

че это?

спасибо привет Лука

4 ответа

Решение

Вот вывод, используя printf вместо:

printf("%.15f\t%.15f\t%.15f\n", a, a + 0.5, floor(a + 0.5));

Неточность теперь ясна:

10.000000000000000  10.500000000000000  10.000000000000000
10.100000000000000  10.600000000000000  10.000000000000000
10.199999999999999  10.699999999999999  10.000000000000000
10.299999999999999  10.799999999999999  10.000000000000000
10.399999999999999  10.899999999999999  10.000000000000000
10.499999999999998  10.999999999999998  10.000000000000000
10.599999999999998  11.099999999999998  11.000000000000000
10.699999999999998  11.199999999999998  11.000000000000000
10.799999999999997  11.299999999999997  11.000000000000000
10.899999999999997  11.399999999999997  11.000000000000000

Добавляя 0,1, вы действительно добавляете значение чуть ниже 0,1.

Таким образом, добавление 0,1 5 раз - это не то же самое, что добавление 0,5 один раз; вы не достигаете этого значения точно. И, добавив 0,5 снова, вы не получите больше 11, что приводит к поведению, которое вы наблюдали.


Программа переменного тока, такая как

#include <stdio.h>
#include <math.h>

int main()
{
    double a = 10.0;
    int i;
    for (i = 0; i < 11; i++) {
        printf("%4.19f\t%4.19f\t%4.19f\n", a, a+.5, floor(a + 0.5));
        a += 0.1;
    }
    printf("\n");
    for (i = 0; i < 11; i++) {
        a = 10.0 + i/10.0;
        printf("%4.19f\t%4.19f\t%4.19f\n", a, a+.5, floor(a + 0.5));
    }
}

показывает на своем выходе

10.0000000000000000000  10.5000000000000000000  10.0000000000000000000
10.0999999999999996447  10.5999999999999996447  10.0000000000000000000
10.1999999999999992895  10.6999999999999992895  10.0000000000000000000
10.2999999999999989342  10.7999999999999989342  10.0000000000000000000
10.3999999999999985789  10.8999999999999985789  10.0000000000000000000
10.4999999999999982236  10.9999999999999982236  10.0000000000000000000
10.5999999999999978684  11.0999999999999978684  11.0000000000000000000
10.6999999999999975131  11.1999999999999975131  11.0000000000000000000
10.7999999999999971578  11.2999999999999971578  11.0000000000000000000
10.8999999999999968026  11.3999999999999968026  11.0000000000000000000
10.9999999999999964473  11.4999999999999964473  11.0000000000000000000

10.0000000000000000000  10.5000000000000000000  10.0000000000000000000
10.0999999999999996447  10.5999999999999996447  10.0000000000000000000
10.1999999999999992895  10.6999999999999992895  10.0000000000000000000
10.3000000000000007105  10.8000000000000007105  10.0000000000000000000
10.4000000000000003553  10.9000000000000003553  10.0000000000000000000
10.5000000000000000000  11.0000000000000000000  11.0000000000000000000
10.5999999999999996447  11.0999999999999996447  11.0000000000000000000
10.6999999999999992895  11.1999999999999992895  11.0000000000000000000
10.8000000000000007105  11.3000000000000007105  11.0000000000000000000
10.9000000000000003553  11.4000000000000003553  11.0000000000000000000
11.0000000000000000000  11.5000000000000000000  11.0000000000000000000

разница: 1-й прогон - это ваш подход с кумулятивной ошибкой и шагом с 0,0999999999999996447, в то время как 2-й прогон пересчитывает a как можно ближе, позволяя точно достичь 10,5 и 11,0.

Проблема в том, что, как уже отмечали другие, у вас никогда 10.5; у вас есть только то, что очень близко к 10.5 (но очень немного меньше).

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

for ( int i = 0; i != 10; ++ i ) {
    double aa = a + ( i / 10. );
    std::cout << aa << '\t' << aa + 0.5 << '\t' << floor( aa + 0.5 ) << std::endl;
}

Это должно дать вам желаемые результаты.

Конечно, если ваш пример - только тест... Многое зависит от того, как вычисляется округляемое значение. Фактическое округление, которое вы используете, может быть подходящим. Или если вы знаете, что значения должны быть кратны 0.1Вы можете попытаться выполнить арифметику, масштабированную на 10, затем округлить результаты, затем округлить до кратного 10.

Проблема связана с накоплением ошибок округления. Числа с плавающей точкой представлены внутри не как целые числа, и их значения в основном приблизительны. Таким образом, вы накапливаете ошибку округления каждый раз, когда выполняете операции += .1 и + .5, и результат - это то, что вы получаете.

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

a = 10. + .1 * i;
Другие вопросы по тегам