Смекни!
smekni.com

Общие представления о языке Java 5 (стр. 31 из 68)

Оператор цикла for

for(блок инициализации; условие выполнения тела цикла;

блок изменения счётчиков)

оператор;

В блоке инициализации через запятую перечисляются операторы задания локальных переменных, область существования которых ограничивается оператором for. Также могут быть присвоены значения переменным, заданным вне цикла. Но инициализация может происходить только для переменных одного типа.

В блоке условия продолжения цикла проверяется выполнение условия, и если оно выполняется, идёт выполнение тела цикла, в качестве которого выступает оператор. Если же не выполняется – цикл прекращается, и идёт переход к оператору программы, следующему за оператором for.

После каждого выполнения тела цикла (очередного шага цикла) выполняются операторы блока изменения счётчиков. Они должны разделяться запятыми.

Пример:

for(int i=1,j=5; i+j<100; i++,j=i+2*j){

...

};

Каждый из блоков оператора for является необязательным, но при этом разделительные “;” требуется писать.

Наиболее употребительное использование оператора for – для перебора значений некоторой переменной, увеличивающихся или уменьшающихся на 1, и выполнения последовательности операторов, использующих эти значения. Переменная называется счетчиком цикла, а последовательности операторов – телом цикла.

Пример1: вычисление суммы последовательно идущих чисел.

Напишем цикл, в котором производится суммирование всех чисел от 1 до 100. Результат будем хранить в переменной result.

int result=0;

for(int i=1; i<=100; i++){

result=result+i;

};

Цикл (повторное выполнение одних и тех же действий) выполняется следующим образом:

- До начала цикла создаётся переменная result, в которой мы будем хранить результат. Одновременно выполняется инициализация – присваивается начальное значение 0.

- Начинается цикл. Сначала выполняется блок инициализации - счётчику цикла i присваивается значение 1. Блок инициализации выполняется только один раз в самом начале цикла.

- Начинается первый шаг цикла. Проверяется условие выполнения цикла. Значение i сравнивается со 100.

- Поскольку сравнение 1<=100 возвращает true, выполняется тело цикла. В переменной result хранится 0, а значение i равно 1, поэтому присваивание result=result+i эквивалентно result=1. Таким образом, после первого шага цикла в переменной result будет храниться значение 1.

- После выполнения тела цикла выполняется секция изменения счётчика цикла, то есть оператор i++, увеличивающий i на 1. Значение i становится равным 2.

- Начинается второй шаг цикла. Проверяется условие выполнения тела цикла. Поскольку сравнение 2<=100 возвращает true, идёт очередное выполнение тела цикла, а затем – увеличение счётчика цикла.

- Шаги цикла продолжаются до тех пор, пока счётчик цикла не станет равным 101. В этом случае условие выполнения тела цикла 101<=100 возвращает false, и происходит выход из цикла. Последнее присваивание result=result+i, проведённое в цикле, это result=result+100.

Если бы нам надо было просуммировать числа от 55 до 1234, в блоке инициализации i надо присвоить 55, а в условии проверки поставить 1234 вместо 100.

Пример 2: вычисление факториала.

double x=1;

for(i=1;i<=n;i++){

x=x*i;

};

Заметим, что в приведённых примерах можно сделать некоторые усовершенствования -операторы присваивания записать следующим образом:

result+=i; вместо result=result+i;

для первого примера и

x*=i; вместо x=x*i;

для второго. На начальных стадиях обучения так лучше не делать, поскольку текст программы должен быть понятен программисту – все алгоритмы должны быть “прозрачны” для понимания.

Наиболее распространённая ошибка при работе с циклами, в том числе – с циклом for - использование вещественного счётчика цикла. Разберём эту ошибку на примере.

Пример 3. Вычисление площади под кривой.

Пусть надо вычислить площадь S под кривой, задаваемой функцией f(x), на промежутке от a до b (провести интегрирование). Разобьём промежуток на n интервалов, при этом длина каждого интервала будет h=(b-a)/n. Мы предполагаем, что величины a,b,n и функция f(x) заданы. Площадь под кривой на интервале с номером j будем считать равной значению функции на левом конце интервала. Такой метод численного нахождения интеграла называется методом левых прямоугольников.

На первый взгляд можно записать алгоритм вычисления этой площади в следующем виде:

double S=0;

double h=(b-a)/n;

for(double x=a;x<b;x=x+h){

S=S+f(x)*h;

};

И действительно, ИНОГДА такой алгоритм правильно работает. Но изменение числа интервалов n или границ a или b может привести к тому, что будет учтён лишний интервал, находящийся справа от точки b. Это связано с тем, что в циклах с вещественным счётчиком ВСЕГДА проявляется нестабильность, если последняя точка попадает на границу интервала. Что будет происходить на последних шагах нашего цикла?

На предпоследнем шаге мы попадаем в точку x=b-h, при этом условие x<b всегда выполняется, и никаких проблем не возникает. Следующей точкой должна быть x=b. Проверяется условие x<b, и в идеальном случае должен происходить выход из цикла, поскольку x==b, и условие не должно выполняться. Но ведь все операции в компьютере для чисел в формате с плавающей точкой проводятся с конечной точностью. Поэтому практически всегда значение x для этого шага будет либо чуть меньше b, либо чуть больше. Отличие будет в последних битах мантиссы, но этого окажется достаточно для того, чтобы сравнение x<b иногда давало true. Хотя для заданных a, b и n результат будет прекрасным образом воспроизводиться, в том числе – на других компьютерах. Более того, при увеличении числа разбиений n погрешность вычисления площади даже для “неправильного” варианта будет убывать, хотя и гораздо медленнее, чем для “правильного”. Это один из самых неприятных типов ошибок, когда алгоритм вроде бы работает правильно, но для получения нужной точности требуется гораздо больше времени или ресурсов.

Рассмотрим теперь правильную реализацию алгоритма. Для этого будем использовать ЦЕЛОЧИСЛЕННЫЙ счётчик цикла. Нам потребуется чуть больше рассуждений, и алгоритм окажется немного менее прозрачным, но зато гарантируется его устойчивость. Значение функции в начале интервала с номером j будет равна f(a+j*h). Первый интервал будет иметь номер 0, второй – номер 1, и так далее. Последний интервал будет иметь номер n-1.

Правильно работающий алгоритм может выглядеть так:

double S=0;

double h=(b-a)/n;

for(int j=0;j<=n-1;j++){

S=S+f(a+j*h)*h;

};

Проверить неустойчивость первого алгоритма и устойчивость второго можно на примере f(x)=x2, a=0, b=1.

n S для первого алгоритма S для второго алгоритма
8 0.2734375 0.2734375
9 0.2798353909465021 0.279835390946502
10 0.3849999999999999 0.2850000000000001
11 0.28925619834710753 0.28925619834710750
12 0.29282407407407407 0.292824074074074
13 0.3727810650887573 0.2958579881656805
100 0.32835000000000036 0.32835000000000014
101 0.32839917655131895 0.3283991765513185
102 0.33825131359733385 0.3284473920287069
103 0.3382034121971908 0.3284946743331133
1000 0.33283350000000095 0.33283350000000034
1001 0.3338330001666631 0.33283399916766554
1002 0.3328344973393309 0.33283449733932

В таблице жирным выделены первые значащие цифры неправильных значений, получающихся в результате неустойчивости алгоритма при n=10, n=13, n=102, n=103, n=1001. При отсутствии неустойчивости оба алгоритма при всех n должны были бы давать одинаковые результаты (с точностью до нескольких младших бит мантиссы). Очень характерной особенностью такого рода неустойчивостей является скачкообразное изменение результата при плавном изменении какого-либо параметра. В приведённой выше таблице меняется число n, но такая же ситуация будет наблюдаться и при плавном изменении чисел a или b. Например, при n=10 и a=0 получим следующие результаты в случае очень малых изменений b:

b S для первого алгоритма S для второго алгоритма
1.00000 0.3849999999999999 0.2850000000000001
1.00001 0.28500855008550036 0.28500855008550036
1.00002 0.2850171003420023 0.2850171003420022
1.00003 0.38503465103951023 0.28502565076950764
1.00004 0.2850342013680182 0.2850342013680184
1.00005 0.2850427521375357 0.2850427521375357

Вещественный счётчик цикла не всегда приводит к проблемам. Рассмотрим вариант численного нахождения интеграла методом средних прямоугольников. Для этого площадь под кривой на интервале с номером j будем считать равной значению функции в середине интервала. Алгоритм идентичен описанному выше для метода левых прямоугольников, за исключением выбора в качестве начальной точки a+h/2 вместо a.

double S=0;

h=(b-a)/n;

for(double x=a+h/2;x<b;x=x+h){

S=S+f(x)*h;

};

Для данного алгоритма проблем не возникает благодаря тому, что все точки x, в которых производится сравнение x<b, отстоят достаточно далеко от значения b – по крайней мере на h/2. Заметим, что метод средних прямоугольников гораздо точнее метода левых прямоугольников, и в реальных вычислениях лучше использовать либо его, либо метод Симпсона, который несколько сложнее, но обычно ещё более точен.

Отметим ещё один момент, важный для эффективной организации циклов. Предложенные выше реализации циклов не самые эффективные по скорости, поскольку в них много раз повторяется достаточно медленная операция – “вещественное” умножение (умножение в формате с плавающей точкой). Лучше написать алгоритм метода средних прямоугольников так:

double S=0;

double h=(b-a)/n;

for(double x=a+h/2;x<b;x=x+h){

S=S+f(x);

};

S=S*h;

При этом суммируются значения f(x), а не f(x)*h, а умножение делается только один раз – уже после выхода из цикла для всей получившейся суммы. Если время выполнения “вещественного”умножения tумнож, то экономия времени работы программы по сравнению с первоначальным вариантом будет (n-1)* tумнож. Для больших значений n экономия времени может быть существенной даже для мощных компьютеров.