logo
Методичка Java

Оператор цикла 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;

};

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

Если бы нам надо было просуммировать числа от 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 экономия времени может быть существенной даже для мощных компьютеров.

Такого рода действия очень характерны при написании программ с использованием циклов, и особенно важны при большом количестве шагов: следует выносить из циклов все операции, которые могут быть проделаны однократно вне цикла. Но при усовершенствованиях часто теряется прозрачность алгоритмов. Поэтому полезно сначала написать реализацию алгоритма “один к одному” по формулам, без всяких усовершенствований, и убедиться при не очень большом числе шагов, что всё работает правильно. А уже затем можно вносить исправления, повышающие скорость работы программы в наиболее критических местах. Не следует сразу пытаться написать программу, которая максимально эффективна по всем параметрам. Это обычно приводит к гораздо более длительному процессу поиска неочевидных ошибок в такой программе.

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

java.util.List components= java.util.Arrays.asList(this.getComponents());

for (Iterator iter = components.iterator();iter.hasNext();) {

Object elem = (Object) iter.next();

javax.swing.JOptionPane.showMessageDialog(null,"Компонент: "+

elem.toString());

}