Смекни!
smekni.com

INT KEYCOUNT;

\) KEYTAB [NKEYS];

оперделяет массив KEYTAB структур такого типа и отводит для них память. Каждый элемент массива является структурой. Это можно было бы записать и так:

STRUCT KEY \( CHAR *KEYWORD;

INT KEYCOUNT;

\);

STRUCT KEY KEYTAB [NKEYS];

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

STRUCT KEY \( CHAR *KEYWORD;

INT KEYCOUNT;

\) KEYTAB[] =\( “BREAK”, 0, “CASE”, 0, “CHAR”, 0, “CONTINUE”, 0, “DEFAULT”, 0,

/* ... */ “UNSIGNED”, 0, “WHILE”, 0

\);

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

\( “BREAK”, 0 \), \( “CASE”, 0 \),

. . .

Но когда инициализаторы являются простыми переменными или символьными строками и все они присутствуют, то во внутренних фигурных скобках нет необходимости. Как обычно, компилятор сам вычислит число элементов массива KEYTAB, если инициализаторы присутствуют, а скобки [] оставлены пустыми.

Программа подсчета ключевых слов начинается с определения массива KEYTAB. ведущая программа читает свой файл ввода, последовательно обращаясь к функции GETWORD, которая извлекает из ввода по одному слову за обращение. Каждое слово ищется в массиве KEYTAB с помощью варианта функции бинарного поиска, написанной нами в главе 3. (Конечно, чтобы эта функция работала, список ключевых слов должен быть расположен в порядке возрастания).

#DEFINE MAXWORD 20

MAIN() /* COUNT “C” KEYWORDS */

\( INT N, T;

CHAR WORD[MAXWORD];

WHILE ((T = GETWORD(WORD,MAXWORD)) != EOF) IF (T == LETTER) IF((N = BINARY(WORD,KEYTAB,NKEYS)) >= 0) KEYTAB[N].KEYCOUNT++;

FOR (N =0; N < NKEYS; N++) IF (KEYTAB[N].KEYCOUNT > 0) PRINTF(“%4D %S&bsol;N”, KEYTAB[N].KEYCOUNT, KEYTAB[N].KEYWORD);

&bsol;) BINARY(WORD, TAB, N) /* FIND WORD IN TAB[0]...TAB[N-1] */ CHAR *WORD;

STRUCT KEY TAB[];

INT N;

&bsol;( INT LOW, HIGH, MID, COND;

LOW = 0;

HIGH = N - 1;

WHILE (LOW <= HIGH) &bsol;( MID = (LOW+HIGH) / 2;

IF((COND = STRCMP(WORD, TAB[MID].KEYWORD)) < 0) HIGH = MID - 1;

ELSE IF (COND > 0) LOW = MID + 1;

ELSE RETURN (MID);

&bsol;) RETURN(-1);

&bsol;) Мы вскоре приведем функцию GETWORD; пока достаточно сказать, что она возвращает LETTER каждый раз, как она находит слово, и копирует это слово в свой первый аргумент.

135

Величина NKEYS - это количество ключевых слов в массиве KEYTAB . Хотя мы можем сосчитать это число вручную, гораздо легче и надежнее поручить это машине, особенно в том случае, если список ключевых слов подвержен изменениям. Одной из возможностей было бы закончить список инициализаторов указанием на нуль и затем пройти в цикле сквозь массив KEYTAB, пока не найдется конец.

Но, поскольку размер этого массива полностью определен к моменту компиляции, здесь имеется более простая возможность.

Число элементов просто есть

SIZE OF KEYTAB / SIZE OF STRUCT KEY дело в том, что в языке “C” предусмотрена унарная операция SIZEOF, выполняемая во время компиляции, которая позволяет вычислить размер любого объекта. Выражение

SIZEOF(OBJECT) выдает целое, равное размеру указанного объекта. (Размер определяется в неспецифицированных единицах, называемых “байтами”, которые имеют тот же размер, что и переменные типа CHAR). Объект может быть фактической переменной, массивом и структурой, или именем основного типа, как INT или DOUBLE, или именем производного типа, как структура. В нашем случае число ключевых слов равно размеру массива, деленному на размер одного элемента массива. Это вычисление используется в утверждении #DEFINE для установления значения NKEYS:

#DEFINE NKEYS (SIZEOF(KEYTAB) / SIZEOF(STRUCT KEY)) Теперь перейдем к функции GETWORD. Мы фактически написали более общий вариант функции GETWORD, чем необходимо для этой программы, но он не на много более сложен. Функция GETWORD возвращает следующее “слово” из ввода, где словом считается либо строка букв и цифр, начинающихся с буквы, либо отдельный символ. Тип объекта возвращается в качетве значения функции; это - LETTER, если найдено слово, EOF для конца файла и сам символ, если он не буквенный.

GETWORD(W, LIM) /* GET NEXT WORD FROM INPUT */ CHAR *W;

INT LIM;

&bsol;( INT C, T;

IF (TYPE(C=*W++=GETCH()) !=LETTER) &bsol;( *W='&bsol;0';

RETURN©;

&bsol;)

WHILE (--LIM > 0) &bsol;( T = TYPE(C = *W++ = GETCH());

IF (T ! = LETTER && T ! = DIGIT) &bsol;( UNGETCH©;

BREAK;

&bsol;)

&bsol;) *(W-1) - '&bsol;0';

RETURN(LETTER);

&bsol;)

Функция GETWORD использует функции GETCH и UNGETCH, которые мы написали в главе 4: когда набор алфавитных символов прерывается, функция GETWORD получает один лишний символ. В результате вызова UNGETCH этот символ помещается назад во ввод для следующего обращения.

Функция GETWORD обращается к функции TYPE для определения типа каждого отдельного символа из файла ввода. Вот вариант, справедливый только для алфавита ASCII.

TYPE© /* RETURN TYPE OF ASCII CHARACTER */ INT C;

&bsol;( IF (C>= 'A' && C<= 'Z' &bsol;!&bsol;! C>= 'A' && C<= 'Z') RETURN(LETTER);

ELSE IF (C>= '0' && C<= '9') RETURN(DIGIT);

ELSE RETURN©;

&bsol;)

Символические константы LETTER и DIGIT могут иметь любые значения, лишь бы они не вступали в конфликт с символами, отличными от буквенно-цифровых, и с EOF; очевидно возможен следующий выбор

#DEFINE LETTER 'A'

#DEFINE DIGIT '0'

функция GETWORD могла бы работать быстрее, если бы обращения к функции TYPE были заменены обращениями к соответствующему массиву TYPE[ ]. В стандартной библиотеке языка “C” предусмотрены макросы ISALPHA и ISDIGIT, действующие необходимым образом.

Упражнение 6-1.

Сделайте такую модификацию функции GETWORD и оцените, как изменится скорость работы программы.

Упражнение 6-2.

Напишите вариант функции TYPE, не зависящий от конкретного наборасимволов.

137

Упражнение 6-3.

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

6.4. Указатели на структуры.

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

Внешнее описание массива KEYTAB не нужно изменять, но функции MAIN и BINARY требуют модификации.

MAIN() /* COUNT C KEYWORD; POINTER VERSION */

&bsol;( INT T;

CHAR WORD[MAXWORD];

STRUCT KEY *BINARY(), *P;

WHILE ((T = GETWORD(WORD, MAXWORD;) !=EOF) IF (T==LETTER) IF ((P=BINARY(WORD,KEYTAB,NKEYS)) !=NULL) P->KEYCOUNT++;

FOR (P=KEYTAB; P>KEYTAB + NKEYS; P++) IF (P->KEYCOUNT > 0) PRINTF(“%4D %S/N”, P->KEYCOUNT, P->KEYWORD);

&bsol;) STRUCT KEY BINARY(WORD, TAB, N) / FIND WORD */ CHAR WORD / IN TAB[0]...TAB[N-1] */ STRUCT KEY TAB [];

INT N;

&bsol;( INT COND;

STRUCT KEY *LOW = &TAB[0];

STRUCT KEY *HIGH = &TAB[N-1];

STRUCT KEY *MID;

WHILE (LOW <= HIGH) &bsol;( MID = LOW + (HIGH-LOW) / 2;

IF ((COND = STRCMP(WORD, MID->KEYWORD)) < 0) HIGH = MID - 1;

ELSE IF (COND > 0) LOW = MID + 1;

ELSE RETURN(MID);

&bsol;) RETURN(NULL);

&bsol;)

Здесь имеется несколько моментов, которые стоит отметить. Во-первых, описание функции BINARI должно указывать, что она возвращает указатель на структуру типа KEY, а не на целое; это объявляется как в функции MAIN, так и в BINARY.

Если функция BINARI находит слово, то она возвращает указатель на него; если же нет, она возвращает NULL.

Во-вторых, все обращения к элементам массива KEYTAB осуществляются через указатели. Это влечет за собой одно существенное изменение в функции BINARY: средний элемент больше нельзя вычислять просто по формуле

MID = (LOW + HIGH) / 2 потому что сложение двух указателей не дает какого-нибудь полезного результата (даже после деления на 2) и в действительности является незаконным. эту формулу надо заменить на

MID = LOW + (HIGH-LOW) / 2 в результате которой MID становится указателем на элемент, расположенный посередине между LOW и HIGH.

Вам также следует разобраться в инициализации LOW и HIGH. указатель можно инициализировать адресом ранее определенного объекта; именно как мы здесь и поступили.

В функции MAIN мы написали FOR (P=KEYTAB; P < KEYTAB + NKEYS; P++) Если P является указателем структуры, то любая арифметика с P учитывает фактический размер данной структуры, так что P++ увеличивает P на нужную величину, в результате чего P указывает на следующий элемент массива структур. Но не считайте, что размер структуры равен сумме размеров ее членов, - из-за требований выравнивания для различных объектов в структуре могут возникать “дыры”.

И, наконец, несколько второстепенный вопрос о форме записи программы. Если возвращаемая функцией величина имеет тип, как, например, в

STRUCT KEY *BINARY(WORD, TAB, N) Tо может оказаться, что имя функции трудно выделить среди текста. В связи с этим иногда используется другой стиль записи:

STRUCT KEY * BINARY(WORD, TAB, N) Это главным образом дело вкуса; выберите ту форму, которая вам нравится, и придерживайтесь ее.

6.5. Структуры, ссылающиеся на себя.

Предположим, что нам надо справиться с более общей задачей, состоящей в подсчете числа появлений всех слов в некотором файле ввода. Так как список слов заранее не известен, мы не можем их упорядочить удобным образом и использовать бинарный поиск. Мы даже не можем осуществлять последовательный просмотр при поступлении каждого слова, с тем чтобы установить, не встречалось ли оно ранее; такая программа будет работать вечно. (Более точно, ожидаемое время работы растет как квадрат числа вводимых слов). Как же нам организовать программу, чтобы справиться со списком произвольных слов?

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

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

Каждому новому слову соответствует один “узел” дерева;

каждый узел содержит: указатель текста слова счетчик числа появлений указатель узла левого потомка указатель узла правого потомка Никакой узел не может иметь более двух детей; возможно отсутсвие детей или наличие только одного потомка.

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