четверг, 4 декабря 2014 г.

Циклы while и for

В цикле

while (выражение)
    инструкция


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

Инструкция for

for (выр1; выр2; выр3)
инструкция


эквивалентна конструкции

выр1;
while (выр2) {
    инструкция
    выр3;
}

если не считать отличий в поведении инструкции continue.

С точки зрения грамматики три компоненты цикла for представляют собой произвольные выражения, но чаще выр1 и выр3 — это присваивания или вызовы функций, а выр2 — выражение отношения. Любое из этих трех выражений может отсутствовать, но точку с запятой опускать нельзя. При отсутствии выр1 или выр3 считается, что их просто нет в конструкции цикла; при отсутствии выр2 предполагается, что его значение как бы всегда истинно. Например,

for (;;) {
...
}

есть "бесконечный" цикл, выполнение которого, вероятно, прерывается каким-то другим способом, например с помощью инструкций break или return.

Какой цикл выбрать: while или for — это дело вкуса. Так, в

while ((с = getchar()) == ' ' || с == '\n' || с == '\t')
    ; /* обойти символы-разделители */

нет ни инициализации, ни пересчета параметра, поэтому здесь больше подходит while.

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

for (i = 0; i < n; i++)
    ...

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

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

Структура программы отражает вид вводимой информации:

игнорировать символы-разделители, если они есть
получить знак, если он есть
взять целую часть и преобразовать ее

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

#include <stdio.h>
#include <ctype.h>
#define MAXLINE 1000    /* максимальный размер вводимой строки */

int getstr(char line[], int maxline);
int atoi(char line[]);

int main(void)
{
int len = 0;
int number = 0;
char line[MAXLINE]; /* текущая строка */

while ((len = getstr(line, MAXLINE)) > 0)
{
number = atoi(line);
printf("Преобразует в число %d\n", number);
number = 0;
}

return 0;
}

/* getline: читает строку в s, возвращает длину */
int getstr(char s[], int lim)
{
int c, i;

for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++i)
s[i] = c;
if (c == '\n')
{
s[i] = c;
++i;
}
s[i] = '\0';        /* в конец строки дописывам "0" */
return i;   /* функция возвращает длину строки */
}

/* atoi: преобразование s в целое число; версия 2 */
int atoi(char s[])
{
int i, n, sign;
/* игнорировать символы-разделители */
for (i = 0; isspace(s[i]); i++)
;
sign = (s[i] == '-') ? -1 : 1;
if (s[i] == '+' || s[i] == '-') /* пропуск знака */
i++;
for (n = 0; isdigit(s[i]); i++)
n = 10 * n + (s[i] - '0');
return sign * n;
}

В стандартной библиотеке имеется более совершенная функция преобразования строки в длинное целое (long int) — функция strtol.


Преимущества, которые дает централизация управления циклом, становятся еще более очевидными, когда несколько циклов вложены друг в друга. Проиллюстрируем их на примере сортировки массива целых чисел методом Шелла, предложенным им в 1959 г. Основная идея этого алгоритма в том, что на ранних стадиях сравниваются далеко отстоящие друг от друга, а не соседние элементы, как в обычных перестановочных сортировках. Это приводит к быстрому устранению массовой неупорядоченности, благодаря чему на более поздней стадии остается меньше работы. Интервал между сравниваемыми элементами постепенно уменьшается до единицы, и в этот момент сортировка сводится к обычным перестановкам соседних элементов. Программа shellsort имеет следующий вид:

#include <stdio.h>
#include <stdlib.h> /* избавление от предупреждений rand, srand */
#include <time.h>

#define MAX 10

void shellsort(int v[], int n);
int binsearch(int x, int v[], int n);

int main(void)
{

int index = 0;
int mass[MAX];
int pos;  /* позиция искомого значения в массиве */
int el = 6;  /* искомое значение */
int k;

/* инициализация текущего времени в качестве "семени" */
srand((unsigned)time(NULL));

for (index = 0; index < MAX; index++)
{
mass[index] = rand() % 100; /* генерация случайных элементов массива от 0 до 100*/
}

for (k = 0; k < MAX; k++)
printf("%2d ", mass[k]);

shellsort(mass, MAX); /* сортировка элементов массива методом Шелла */

printf("\n");
for (k = 0; k < MAX; k++)
printf("%2d ", mass[k]);

pos = binsearch(el, mass, index);
printf("\nЗначение %d имеет индекс %d\n", el, pos);


return 0;
}

/* shellsort: сортируются v[0] ... v[n-1] в возрастающем порядке */
void shellsort(int v[], int n)
{
int gap, i, j, temp;
for (gap = n / 2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i - gap; j >= 0 && v[j] > v[j + gap]; j -= gap) {
temp = v[j];
v[j] = v[j + gap];
v[j + gap] = temp;
}
}

/* binsearch: найти x в v[0] <= v[1] <= ... <= v[n-1] */
int binsearch(int x, int v[], int n)
{
int low, high, mid;

low = 0;
high = n - 1;
while (low <= high) {
mid = (low + high) / 2;
if (x < v[mid])
high = mid - 1;
else if (x > v[mid])
low = mid + 1;
else  /* совпадение найдено */
return mid;
}

return -1;  /* совпадения нет */
}

Здесь использованы три вложенных друг в друга цикла. Внешний управляет интервалом gap между сравниваемыми элементами, сокращая его путем деления пополам от n/2 до нуля. Средний цикл перебирает элементы. Внутренний — сравнивает каждую пару элементов, отстоящих друг от друга на расстоянии gap, и переставляет элементы в неупорядоченных парах. Так как gap обязательно сведется к единице, все элементы в конечном счете будут упорядочены. Следует обратить внимание на то, что универсальность цикла for позволяет сделать внешний цикл по форме похожим на другие, хотя он и не является арифметической прогрессией.

Последний оператор Си — это "," (запятая), которую чаще всего используют в инструкции for. Пара выражений, разделенных запятой, вычисляется слева направо. Типом и значением результата являются тип и значение правого выражения, что позволяет в инструкции for в каждой из трех компонент иметь по нескольку выражений, например вести два индекса параллельно. Авторы книги демонстрируют это на примере функции reverse(s), которая "переворачивает" строку s, оставляя результат в той же строке s:

#include <stdio.h>
#include <string.h>

void reverse(char s[]);

int main(void)
{
char stroka[] = "Hello, world!";
/* Вывод строки */
printf("%s\n", stroka);
reverse(stroka);
/* Вывод перевернутой строки */
printf("%s\n", stroka);

return 0;
}

/* reverse: переворачивает строку s (результат в s) */
void reverse(char s[])
{
int c, i, j;
for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
c = s[i];
s[i] = s[j];
s[j] = c;
}
}

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

Запятыми как операторами следует пользоваться умеренно. Более всего они уместны в конструкциях, которые тесно связаны друг с другом (как в for-цикле программы reverse), а также в макросах, в которых многоступенчатые вычисления должны быть выражены одним выражением. Запятой-оператором в программе reverse можно было бы воспользоваться и при обмене символами в проверяемых парах элементов строки, мысля этот обмен как одну отдельную операцию:

/* reverse: переворачивает строку s (результат в s) */
void reverse(char s[])
{
int c, i, j;
for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
c = s[i] , s[i] = s[j], s[j] = c;
}
}

Комментариев нет:

Отправить комментарий