Подтвердить что ты не робот

Почему этот цикл никогда не заканчивается?

Возможный дубликат:
при сравнении двойных значений в С#

Я читал его в другом месте, но действительно забыл ответ, поэтому я прошу здесь еще раз. Этот цикл никогда не заканчивается, если вы его кодируете на любом языке (я тестирую его на С#, С++, Java...):

double d = 2.0;
while(d != 0.0){
   d = d - 0.2;
}
4b9b3361

Ответ 1

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

double d = 2.0;
while (d != 0.0)
{
    Console.WriteLine(d);
    d = d - 0.2;
}
2
1,8
1,6
1,4
1,2
1
0,8
0,6
0,4
0,2
2,77555756156289E-16   // Not exactly zero!!
-0,2
-0,4

Один из способов решения этой проблемы - использовать тип decimal.

Ответ 2

(Во-первых, вы не используете одну и ту же переменную, но я предполагаю, что опечатка:)

0,2 не очень 0,2. Это самое близкое значение double до 0,2. Когда вы вычтете это 10 раз из 2.0, вы не получите ровно 0,0.

В С# вы можете вместо этого использовать тип decimal, который будет работать:

// Works
decimal d = 2.0m;
while (d != 0.0m) {
   d = d - 0.2m;
}

Это работает, потому что десятичный тип представляет десятичные значения, такие как 0,2 точно (в пределах, это 128-битный тип). Каждое задействованное значение точно представимо, поэтому оно работает. Что бы не получилось:

decimal d = 2.0m;
while (d != 0.0m) {
   d = d - 1m/3m;
}

Здесь "третий" не является точно представимым, поэтому мы получаем ту же проблему, что и раньше.

В целом, однако, это плохая идея для выполнения точных сравнений между числами с плавающей запятой - обычно вы сравниваете их с определенным допуском.

У меня есть статьи о плавающая двоичная точка и плавающая десятичная точка из контекста С#/. NET, которые более подробно объясняют ситуацию.

Ответ 3

Я помню, как покупал Sinclair ZX-81, прокладывая себе путь через превосходное руководство по программированию Basic и почти возвращаясь в магазин, когда я столкнулся с первой ошибкой округления с плавающей точкой.

Я бы никогда не подумал, что у людей все еще будут проблемы 27,99998 лет.

Ответ 4

Вам лучше использовать

while(f  > 0.0) 

* edit: См. комментарий pascal ниже. Но если вам нужно запустить цикл интегральным, детерминированным числом раз, скорее используйте целостный тип данных.

Ответ 5

Проблема - арифметика с плавающей запятой. Если для числа нет точного двоичного представления, вы можете сохранить только его ближайший номер (так же, как вы не смогли сохранить число 1/3 в десятичной форме - вы можете хранить только что-то вроде 0.33333333 для некоторой длины '3's.) Это означает, что арифметика чисел с плавающей запятой довольно часто не совсем точна. Попробуйте что-то вроде следующего (Java):

public class Looping {

    public static void main(String[] args) {

        double d = 2.0;
        while(d != 0.0 && d >= 0.0) {
            System.out.println(d);
            d = d - 0.2;
        }

    }

}

Ваш результат должен выглядеть примерно так:

2.0
1.8
1.6
1.4000000000000001
1.2000000000000002
1.0000000000000002
0.8000000000000003
0.6000000000000003
0.4000000000000003
0.2000000000000003
2.7755575615628914E-16

И теперь вы должны уметь видеть, почему условие d == 0 никогда не происходит. (последнее число - это число очень, близкое к 0, но не совсем.

Для другого примера странности с плавающей запятой попробуйте следующее:

public class Squaring{

    public static void main(String[] args) {

        double d = 0.1;
        System.out.println(d*d);

    }

}

Поскольку нет двоичного представления точно 0.1, возведение в квадрат его не приводит к ожидаемому результату (0.01), но на самом деле что-то вроде 0.010000000000000002!

Ответ 6

f неинициализируется;)

Если вы имеете в виду:

double f = 2.0;

Это может быть эффект неточного точного алгоритма двойных переменных.

Ответ 7

это из-за точности с плавающей запятой. (d > 0.0), или если вы должны,

while (Math.abs(d-0.0) > some_small_value){

}

Ответ 8

Это не останавливается, потому что 0,2 я не точно представлены в двух дополнениях поэтому ваш цикл никогда не выполняет тест 0.0==0.0

Ответ 9

Как говорили другие, это просто фундаментальная проблема, которую вы получаете при выполнении арифметики с плавающей запятой на любую базу. Просто бывает, что base-2 является наиболее распространенным в компьютерах (поскольку он допускает эффективную аппаратную реализацию).

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

int dTimes10 = 20;
double d;
while(dTimes10 != 0) {
   dTimes10 -= 2;
   d = dTimes10 / 10.0;
}

Здесь мы действительно работаем с фракциями [20/10, 18/10, 16/10,..., 2/10, 0/10], где итерация выполняется с целыми числами (т.е. легко получить правильно) в числителе с фиксированным знаменателем, прежде чем перейти к плавающей точке. Если вы можете переписать свои настоящие итерации, чтобы работать так, вы получите большой успех (и они действительно не намного дороже, чем то, что вы делали до этого, что является отличным компромиссом, чтобы получить правильность).

Если вы не можете этого сделать, вам нужно использовать equal-in-epsilon в качестве сравнения. Примерно, что замена d != target на abs(d - target) < ε, где выбор ε (epsilon) иногда может быть неудобным. В принципе, правильное значение ε зависит от совокупности факторов, но он, вероятно, лучше всего выбирается как 0,001 для примера итерации, учитывая масштаб значения шага (т.е. Он составляет половину процента от величины шага, поэтому все, что находится в этом будет ошибкой, а не информативной).