четверг, 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;
}
}

Упражнение 3.2.

Напишите функцию escape(s, t), которая при копировании текста из t в s преобразует такие символы, как новая строка и табуляция в "видимые последовательности символов" (вроде \n и \t). Используйте инструкцию switch. Напишите функцию, выполняющую обратное преобразование эскейп-последовательностей в настоящие символы.

понедельник, 1 декабря 2014 г.

Переключатель switch

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

switch (выражение) {
    case конст-выр: инструкции
    case конст-выр: инструкции
    default: инструкции
}


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

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

#include <stdio.h>

/* подсчет цифр, символов-разделителей и прочих символов */
int main(void)
{
    int c, i, nwhite, nother, ndigit[10];
    nwhite = nother = 0;
    for (i = 0; i < 10; i++)
        ndigit[i] = 0;
    while ((c = getchar()) != EOF) {
        switch (c) {
            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
                ndigit[c - '0']++;
                break;
            case ' ':
            case '\n':
            case '\t':
                nwhite++;
                break;
            default:
                nother++;
                break;
        }
    }
    printf("Цифры =");
    for (i = 0; i < 10; i++)
        printf(" %d", ndigit[i]);
    printf(", символы-разделители = %d, прочие = %d\n", nwhite, nother);
    return 0;
}
$ cc -g -O0 -Wall -o a.out count.c
$ ./a.out
test 1234567890 123 12 1 1      1

Цифры = 1 6 3 2 1 1 1 1 1 1, символы-разделители = 7, прочие = 4


Инструкция break вызывает немедленный выход из переключателя switch. Поскольку выбор ветви case реализуется как переход на метку, то после выполнения одной ветви case, если ничего не предпринять, программа провалится вниз на следующую ветвь. Инструкции break и return — наиболее распространенные средства выхода из переключателя. Инструкция break используется также для принудительного выхода из циклов while, for и do-while.

пятница, 28 ноября 2014 г.

Упражнение 3.1.

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

среда, 26 ноября 2014 г.

Конструкция else-if

Последовательность инструкций

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

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

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

else
    инструкция

можно опустить или использовать для фиксации ошибочной ("невозможной") ситуации.

В качестве иллюстрации трехпутевого ветвления авторы книги предлагают рассмотреть функцию бинарного поиска значения x в массиве v. Предполагается, что элементы v упорядочены по возрастанию. Функция выдает положение x в v (число в пределах от 0 до n-1 ), если x там встречается, и -1 , если его нет.

При бинарном поиске значение x сначала сравнивается с элементом, занимающим серединное положение в массиве v. Если x меньше, чем это значение, то областью поиска становится "верхняя" половина массива v, в противном случае — "нижняя". В любом случае следующий шаг — это сравнение с серединным элементом отобранной половины. Процесс "уполовинивания" диапазона продолжается до тех пор, пока либо не будет найдено значение, либо не станет пустым диапазон поиска.
$ vim binsearch.c
#include <stdio.h>

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

int main(void)
{
    int index = 10;
    int arr[10] = { 1, 3, 4, 7, 15, 18, 33, 34, 67, 88 }; /* упорядоченный по возрастанию массив из 10 элементов */
    int pos;  /* позиция искомого значения в массиве */
    int el = 7;  /* искомое значение */

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

    return 0;
}

/* 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;  /* совпадения нет */
}
$ cc -g -O0 -Wall -o a.out binsearch.c
$ ./a.out
Значение 7 имеет индекс 3

Основное действие, выполняемое на каждом шаге поиска, — сравнение значения x (меньше, больше или равно) с элементом v[mid]; это сравнение естественно поручить конструкции else-if.

понедельник, 24 ноября 2014 г.

Конструкция if-else

Инструкция if-else используется для принятия решения. Формально ее синтаксисом является:

if (выражение)
    инструкция1
else
    инструкция2

причем else-часть может и отсутствовать. Сначала вычисляется выражение, и, если оно истинно (т. е. отлично от нуля), выполняется инструкция1. Если выражение ложно (т. е. его значение равно нулю) и существует else-часть, то выполняется инструкция2.

Так как if просто проверяет числовое значение выражения, условие иногда можно записывать в сокращенном виде. Так, запись

if (выражение)
короче, чем

if (выражение != 0)

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

if (n > 0)
    if (а > Ь)
        z = а;
    else
        z = b;


else относится к внутреннему if, что и показано с помощью отступов.

Если требуется иная интерпретация, необходимо должным образом расставить фигурные скобки:

if (n > 0) {
    if (a > b)
        z = а;
    }
else
    z = b;

По правилам грамматики за if должна следовать инструкция, а выражение-инструкция вроде z = а; всегда заканчивается точкой с запятой.

Инструкции и блоки

Порядок, в котором выполняются вычисления, определяется инструкциями управления.

Выражения, х = 0, или i++, или printf (...) , становятся инструкциями, если в конце них поставить точку с запятой, например:

х = 0;

i++;

printf (...);

В Си точка с запятой является заключающим символом инструкции.

Фигурные скобки { и } используются для объединения объявлений и инструкций в составную инструкцию, или блок, чтобы с точки зрения синтаксиса эта новая конструкция воспринималась как одна инструкция.

Фигурные скобки, обрамляющие группу инструкций, образующих тело функции, — это один пример; второй пример — это скобки, объединяющие инструкции, помещенные после if, else, while или for.