Найти тему
Геннадий Шушпанов

Числа и программирование

Оглавление

Чему равен (√2)²

Напишем небольшую программу. Каждый из вас может использовать свой любимый язык.

a = Sqrt(2);
WriteLine(a*a);

Здесь Sqrt -- функция извлечения квадратного корня, а WriteLine выводит в консоль значение параметра. Запустите и посмотрите, что вы получите. У меня на C# вышло 2,0000000000000004. Почему так?

Не все числа можно представить

Корень из двух -- бесконечная дробь, а для записи значения числа в памяти компьютера отводится конечное число байт. У нас нет возможности, представить √2 точно. Поэтому a*a в операторе вывода не равно (√2)².

С корнем из двух проблема ожидаема, поскольку это иррациональное число. Но возьмем небольшое рациональное: 5/100. Переведем его в двоичную систему:

0.05₁₀ ≈ 0.00001100110011001100110011001100110011001100110011₂

Получается периодическая бесконечная дробь. Выходит мы не можем представить точно и это число. Это приводит к пониманию, что числовая прямая компьютера не совпадает с привычной нам в математике, она не является непрерывной. Результатом выражения может быть не представимое число и будет использовано его приближение.

Не всё одинаковое равно

Поэкспериментируем с числом 0.05 (программа на C#).

float x = 0.05f;
double y = 0.05;
decimal z = 0.05M;
Console.WriteLine(x == y);
Console.WriteLine(x == z);
Console.WriteLine(y == z);

Что получим в результате? Ничего -- второй и третий операторы вывода не скомпилируются с сообщением, что "==" к данным операндам не применим. Если их убрать, то оставшийся выведет false. Всё становится ясно, если вспомнить про бесконечную дробь. X и y содержат разные приближения числа 0.05. Вот и не равны.

Скачущее округление

В Java Script есть функция toFixed, преобразующая число в строку в десятичном представлении с округлением до указанного числа десятичных знаков. Используем ее в простой программе:

console.log(0.325.toFixed(2));
console.log(10.325.toFixed(2));
console.log(100.325.toFixed(2));
console.log(500.325.toFixed(2));

Программа простая, а вот результат озадачивает:

0.33
10.32
100.33
500.32

Однако, если мы увеличим число десятичных знаков до 20, то приоткроем завесу тайны:

0.32500000000000001110
10.32499999999999928946
100.32500000000000284217
500.32499999999998863132

Здесь опять невозможность точного представления приводит к не ожидаемому результату. Стоит однако отметить, что другие языки как-то с этим справляются. Округление в C# для указанного набора дает стабильные 33 после десятичной точки.

Длинные суммы

Возьмем дельту равную 0.001 и просуммируем ее миллион раз. А после сравним сумму с произведением дельты на миллион. На C# с double получились следующие результаты:

Произведение: 1000;
Сумма: 999,9999999832651

Если еще посчитать число случаев, когда сумма была равна произведению дельты на номер итерации, все станет еще печальнее:

Сумма и произведение равны: 29
Сумма и произведение не равны: 999971
Всего итераций: 1000000
Последнее равенство: 292296
Первое расхождение: 10

А вот для дельты равной 0.001953125 все прекрасно:

Произведение: 1953.125;
Сумма: 1953.125
Сумма и произведение равны: 1000000
Сумма и произведение не равны: 0
Всего итераций: 1000000
Последнее равенство: 999999
Первое расхождение: нет расхождений

Секрет такого счастья: 0.001953125 = 1/512 = 1/2⁹ и имеет точное представление.