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;