Опубликован: 04.12.2009 | Доступ: свободный | Студентов: 8416 / 657 | Оценка: 4.30 / 3.87 | Длительность: 27:27:00
Лекция 5:

Управляющие конструкции

< Лекция 4 || Лекция 5: 123 || Лекция 6 >

5.6. Оператор цикла 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)=x^2,\; a=0,\; b=1.

Таблица 5.1.
n S для первого алгоритма S для второго алгоритма
8 0.2734375 0.2734375
9 0.2798353909465021 0.279835390946502
10 0. 38 49999999999999 0.2850000000000001
11 0.28925619834710753 0.28925619834710750
12 0.29282407407407407 0.292824074074074
13 0. 37 27810650887573 0.2958579881656805
100 0.32835000000000036 0.32835000000000014
101 0.32839917655131895 0.3283991765513185
102 0.3 38 25131359733385 0.3284473920287069
103 0.3 38 2034121971908 0.3284946743331133
1000 0.33283350000000095 0.33283350000000034
1001 0.33 38 330001666631 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:

Таблица 5.2.
b S для первого алгоритма S для второго алгоритма
1.00000 0. 38 49999999999999 0.2850000000000001
1.00001 0.28500855008550036 0.28500855008550036
1.00002 0.2850171003420023 0.2850171003420022
1.00003 0. 38 503465103951023 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());
}
< Лекция 4 || Лекция 5: 123 || Лекция 6 >
Полетаев Дмитрий
Полетаев Дмитрий
Не очень понятно про оболочечные Данные,ячейки памяти могут наверно размер менять,какое это значение те же операции только ячейки больше,по скорости тоже самое
Максим Старостин
Максим Старостин

Код с перемещением фигур не стирает старую фигуру, а просто рисует новую в новом месте. Точку, круг.