Добавил:
СПбГУТ * ИКСС * Программная инженерия Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Язык Си. Лабораторные работы / Справочник. Часть 1 (СПбГУТ).doc
Скачиваний:
50
Добавлен:
10.09.2019
Размер:
949.25 Кб
Скачать

2.3. Организация циклов

В настоящем разделе рассматриваются только арифметические циклы. Раздел содержит три подраздела:

● Простые циклы.

● Циклы и разветвления.

● Рекомендации по программированию.

В первом из подразделов (простые циклы) рассматриваются задачи, при программировании которых требуется организовать только один цикл, причем необходимость в использовании разветвлений отсутствует.

Во втором подразделе (циклы и разветвления) рассматриваются все возможные комбинации арифметических циклов и разветвлений.

Раздел заканчивается рекомендациями по решению задач, требующих программирования циклических алгоритмов.

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

2.3.1. Простые циклы

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

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

Пример 1. Отделение многократно и однократно выполняемых вычислений.

Постановка задачи. Вычислить значение величины y, заданной следующим образом:

Решение.

При решении рассматриваемой задачи у некоторых студентов, возможно, возникнут затруднения, связанные с определением в какой части алгоритма извлекать корень – в теле цикла или после его завершения. Для преодоления этих трудностей целесообразно выполнить преобразование исходной формулы, обозначив через s входящую в нее сумму. В результате выполненных преобразований вместо одной исходной формулы получим следующие две расчетные формулы:

Вычисления по первой из формул требуют организации циклического алгоритма для накопления суммы, а вычисления по второй – могут быть выполнены в линейном алгоритме (после цикла). Таким образом, становится очевидным, что корень квадратный следует извлекать только один раз.

Для вычисления по первой из расчетных формул следует организовать арифметический цикл. Задача о накоплении суммы уже рассматривалась в пункте 1.29. Используя методику, изложенную в указанном пункте, можно написать следующую программу для решения рассматриваемой задачи.

#include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double x, y, s;

printf(“n=”) scanf(“%d”, &n); printf(“x=”); scanf(“%lf”, &x); s = 0; for(i = 2 i <= n; i++) { t = i / (i + x); y += t * t; } y = sqrt(s); printf(“y=%0.4g\n”, y); getch(); return 0; }

Заметим в заключение, что с целью упрощения возведения в квадрат в теле цикла введена вспомогательная переменная “t”, в которой запоминается результат вычисления значения выражения i / (i +x).

Пример 2. Чистка цикла

“Чистка” цикла состоит в вынесении из тела цикла действий, результаты вычисления которых в цикле не изменяются. Подобная операция может привести к существенному уменьшению объема вычислений. Особенно полезным это может быть в тех случаях, когда из тела цикла выносятся обращения к библиотечным функциям. Следует учитывать, что чистка цикла может осуществляться компилятором в процессе выполнения оптимизации.

Постановка задачи.

Вычислить значение величины “y”, заданной следующим образом

y =

Решение.

Непосредственное использование исходного выражения, определяющего значение “y”, приводит к организации цикла, имеющего следующий вид

y = 0; for(i=3; i<= n; i++) y += i / (i * i + sqrt(fabs(x)));

Вычисление выражения sqrt(fabs(x)) на каждом шаге цикла будет давать один и тот же результат. С целью уменьшения времени выполнения программы значение этого выражения можно вычислить один раз в линейной части алгоритма, предшествующей циклу. В связи с тем, что для вычисления корня квадратного следует использовать цикл, можно ожидать существенного уменьшения объема вычислений. Этот приём и составляет существо чистки цикла.

#include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double y, x, z;

printf(“n=”) scanf(“%d”, &n); printf(“x=”) scanf(“%lf”, &x); z= sqrt(fabs(x)); y=0; for(i = 3; i <= n; i++) y += i /(i * i + z); printf(“y=%0.4g\n”,y); getch(); return 0; }

Пример 3. Вычисление суммы знакопеременного ряда

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

Наличие сомножителя (-1)i делает ряд знакопеременным. Слежение за знаком очередного слагаемого можно реализовать путем введения вспомогательной переменной (назовем ее znak). Изменение знака можно получить с помощью инструкции следующего вида: znak = - znak;. При рассмотрении вопроса об инициализации переменной znak следует обращать внимание на знак первого слагаемого в искомой сумме. В рассматриваемой задаче переменная znak должна быть инициализирована значением 1. Это обусловлено тем обстоятельством, что первое слагаемое суммы является положительным. Ниже приводится программа, выполняющая необходимые вычисления.

#include<stdio.h> #include<conio.h> int main(void) { int i, n; int s, znak;

printf(“n=”) scanf(“%d”, &n); s = 0; znak = 1; for(i = 2; i <= n; i++) { s += znak * i * i; znak = -znak; } printf(“s=%d\n”, s); getch(); return 0; }

Пример 4. Вычисление очередного слагаемого по рекуррентной формуле.

Пусть необходимо вычислить сумму следующего вида:

.

На первый взгляд, для вычисления рассматриваемой суммы необходимо организовать вложенные циклы. При этом внешний цикл должен накапливать сумму s, а внутренний − вычислять факториал i!. Такой подход имеет ряд недостатков. Первый из них связан с быстрым возрастанием факториала i!, что может привести к переполнению разрядной сетки. B то же время значение очередного слагаемого, определяемого величиной

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

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

s += a;

После этого следует выполнить продвижение цикла. С этой целью необходимо найти очередное значение слагаемого, которое должно быть добавлено к сумме s на следующем шаге цикла. Для этого попытаемся найти связь между двумя последовательными значениями слагаемых. Для этого перейдем к математическому представлению очередного и последующего слагаемых. Обозначим через ai – последнее слагаемое, добавленное в сумму s. Индекс i этого слагаемого соответствует текущему значению управляющей переменной. Тогда через ai + 1 следует обозначить последующее слагаемое. Отношение этих величин примет следующий вид:

.

Отсюда имеем:

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

a *= x / (i + 1);

Определившись с телом цикла, перейдем к рассмотрению его вспомогательных элементов. Наибольшие сложности возникают с инициализацией переменной a. До начала работы цикла ее значение должно быть равно первому слагаемому. Это значение следует вычислить вручную. В рассматриваемом примере для этого необходимо написать следующую инструкцию присваивания:

a = pow(x, 4) / 24;

Приведем теперь программу, выполняющую необходимые вычисления:

#include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double x, s, a; printf(“n=”); scanf(“%d”, &n); printf(“x=”); scanf(“%lf”, &x); s = 0; a = pow(x, 4) / 24; for(i = 4; i <= n; i++) { s += a; a *= x / (i + 1); } printf(“s = %0.4g\n”, s); getch(); return 0; }

Пример 5. Объединение циклов.

В некоторых случаях циклы могут быть объединены. Приведем пример.

Постановка задачи.

Пусть необходимо вычислить значение величины y, заданной следующим образом:

.

Обозначим через s и p соответственно сумму и произведение, входящие в исходную расчетную формулу. Это позволяет вместо одной расчетной формулы написать следующие три формулы:

Выполненное преобразование позволяет в вычислении величины y выделить три этапа. Первые два из них требуют организации циклических вычислительных процессов. Один из них необходим для накопления суммы s, а другой – для накопления произведения p. Завершать вычисления должен линейный вычислительный процесс, предназначенный для вычисления величины y по последней формуле. Решение, соответствующее такому подходу, имеет следующий вид:

/* Первый вариант решения */ #include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double x, s, p, y; printf(“n=”); scanf(“%d”, &n); printf(“x=”); scanf(“%lf”, &x); s = 0; for(i = 2; i <= n; i++) s += i / (i + fabs(x)); p = 1; for(i = 2; i <= n; i++) p *= i / (i + fabs(x)); y = (2 + s) / (3 + p); printf(“y=%g\n”, y); getch(); return 0; }

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

В исходном коде полученной программы обращает на себя внимание похожесть циклов, предназначенных для вычисления суммы и произведения. Действительно, сравниваемые циклы имеют одинаковые заголовки. Кроме того, в теле каждого из циклов выполняется вычисление одного и того же выражения i / (i + fabs(x)). Ниже приводится второй вариант решения поставленной задачи, который предусматривает:

● Объединение циклов накопления суммы и произведения в одном цикле,

● вычленение вычисления выражения i / (i + fabs(x)).

/* Второй вариант решения */ #include<stdio.h> #include<conio.h> #include<math.h> int main(void) { int i, n; double x, s, p, y, a,z; clrscr(); printf(“n=”); scanf(“%d”, &n); printf(“x=”); scanf(“%lf”, &x);

z = fabs(x);

s = 0; p = 1; for(i = 2; i <= n; i++) { a = i / (i + z); s += a; p *= a; } y = (2 + s) / (3 + p); printf(“y=%0.4g\n”, y); getch(); return 0; }

Пример 6. Задача табулирования.

Постановка задачи

Табулировать в “n” равноотстоящих точках функцию y = x2, начиная от начального значения x = xn вплоть до конечного значения x = xk. Будем считать, что аргументом табулируемой функции является переменная вещественного типа.

Решение

Существо рассматриваемой задачи состоит в том, что требуется обеспечить n – кратное вычисление заданной функции, последовательно изменяя значение аргумента “x” от значения, равного x = xn, до x = xk с постоянным шагом dx, для вычисления которого в программе следует предусмотреть инструкцию присваивания следующего вида:

dx = (xk - xn) / (n - 1);

Для решения рассматриваемой задачи следует организовать арифметический цикл, который должен обеспечить n – кратное выполнение однотипных действий.

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

for(x = xn; x <= xk; x += dx)

является нежелательным. Дело в том, что погрешности, присущие представлению данных действительного типа (double или float), не позволяют гарантировать выполнение заданного количества повторений цикла.

Вывод таблицы должен состоять из двух частей:

● вывод заголовка таблицы.

● Вывода строк таблицы, содержащих данные.

Вывод заголовка таблицы следует выполнить в линейной части программы, предшествующей циклу. Вывод очередной (текущей строки) должен находиться в теле цикла. Спецификации преобразования вызовов функции printf() вывода заголовка таблицы и текущей строки должны быть согласованы. Необходимо согласовывать модификаторы минимальной ширины соответствующих спецификации этих вызовов.

Программа, реализующая требуемые вычисления, приведена ниже.

#include<stdio.h> #include<conio.h> int main(void) { int i, n; double x, xn, xk, dx, y; printf(“Количество расчетных точек=”); scanf(“%d”, &n); printf(“Начальное значение аргумента=”); scanf(“%lf”, &xn); printf(“Конечное значение аргумента=”); scanf(“%lf”, &xk) printf(“%5s%15s%15s\n”, “НОМЕР”, “АРГУМЕНТ”, “ФУНКЦИЯ”); x = xn; dx = (xk - xn) / (n - 1); for(i = 1; i <= n; i++) { y = x * x; printf(“%5d%15.4g%15.4g\n”, i, x, y); x += dx; } getch(); return 0; }