Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Kernigan_B__Payk_R_Praktika_programmirovania.pdf
Скачиваний:
76
Добавлен:
18.03.2016
Размер:
2.53 Mб
Скачать

Создание структуры данных в языке С

Начнем с реализации на С. Для начала надо задать некоторые константы:

enum { NPREF = 2,

/* количество слов в префиксе */ NHASH = 4093,

/* размер массива хэш-таблицы состояний */ MAXGEN = 10000

/* максимум генерируемых слов */ };

В этом описании определяются количество слов в префиксе (NPREF), размер массива хэш-таблицы (NHASH) и верхний предел количества генерируемых слов (MAXGEN). Если NPREF — константа времени компиляции, а не переменная времени исполнения, то управление хранением упрощается. Мы задали очень большой размер массива, поскольку программа должна быть способна воспринимать очень большие документы, возможно, даже целые книги. Мы выбрали NHASH = 4093, так что если во введенном . тексте имеется 10 000 различных префиксов (то есть пар слов), то среднестатистическая цепочка будет весьма короткой — два или три префикса. Чем больше размер, тем короче будет ожидаемая длина цепи и, следовательно, тем быстрее будет осуществляться поиск. Эта программа создается для развлечения, поэтому производительность ее не критична, однако если мы сделаем слишком маленький массив, то программа не сможет обработать текст ожидаемого размера за разумное время; если же сделать его слишком большим,'он может не уложиться в имеющуюся память.

Префикс можно хранить как массив слов. Элементы хэш-таблицы будут представлены как тип данных State, ассоциирующий список Suffix с префиксом:

typedef struct State State; typedef struct Suffix Suffix;

struct State { /* список префиксов + суффиксы */ char *pref[NPREFJ; /*

слова префикса */ Suffix *suf; /* список суффиксов */ State *next; /* следующий в хэш-таблице */

};

struct Suffix { /* список суффиксов */ char *word; /* суффикс */

Suffix *next; /* следующий суффикс в списке */ };

State *statetab[NHASH]; /*

хэш-таблица состояний */

Графически структура данных выглядит так:

Нам потребуется хэш-функция для префиксов, которые являются массивами строк. Нетрудно преобразовать хэш-функцию из главы 2, сделав цикл по строкам в массиве, таким образом хэшируя конкатенацию этих строк:

/* hash: вычисляет хэш-значение для массива из NPREF строк */ unsigned int hash(char *s[NPREF])

{

unsigned int h; unsigned char *p;

int i;

/* hash: вычисляет хэш-значение для массива из NPREF строк */ unsigned int hash(char *s[NPREF])

{

unsigned int h; unsigned char *p; int i;

Выполнив схожим образом модификацию алгоритма lookup, мы завершим реализацию хэш-таблицы:

/* lookup: ищет префикс; создает его при необходимости. */ /* возвращает указатель, если префикс существует или создан; *, /* NULL в противном случае. */

/* при создании не делает strdup, так что строки /* не должны изменяться после

помещения в таблицу. */

State* lookup(char *prefix[NPREF],

int create)

{

int i, h; State *sp;

h = hash(prefix);

for (sp = statetabfh]; sp != NULL; sp = sp->next)

{ for (i = 0; i,< NPREF; i++)

if (strcmp(prefix[i], sp->pref[i]) != 0) break;

if (i == NPREF) /* нашли'*/ return sp; I> if (create) { sp = (State *) emalloc(sizeof(State)); for

(i = 0; i < NPREF; i++)

sp->pref[i] = prefix[i]; sp->suf = NULL; sp->next = statetabfh];

statetab[h] = sp; } return sp; }

Обратите внимание на то, что lookup не создает копии входящей строки при создании нового состояния, просто в sp->pref [ ] 'сохраняются указатели. Те, кто использует вызов lookup, должны гарантировать, что впоследствии данные изменены не будут. Например, если строки находятся в буфере ввода-вывода, то перед тем, как вызвать lookup, надо сделать их копию; в противном случае следующие куски вводимого текста могут перезаписать данные, на которые ссылается хэш-таблица. Необходимость решать, кто же владеет совместно используемыми ресурсами, возникает достаточно часто. Эту тему мы подробно рассмотрим в следующей главе.

Теперь нам надо прочитать файл и создать хэш-таблицу:

/* build; читает ввод, создает таблицу префиксов */ void build(char *prefix[NPREF], FILE *f)

{

char buf[100], fmt[10];

/* создать форматную строку:

%s может переполнить buf */ sprintf(fmt, "%%%ds", sizeof(buf)-1); while

(fscanf(f, fmt, buf) != EOF) add(prefix, estrdup(buf)); }

Необычный вызов sprintf связан с досадной проблемой, присущей f scant, — если бы не эта проблема, функция f scanf идеально подошла бы для нашего случая. Все дело в том, что вызов fscanf с форматом %s считывает следующее ограниченное пробелами слово из файла в буфер, но без проверки его длины: слишком длинное слово может переполнить входной буфер, что приведет к разрушительным последствиям. Если буфер имеет размер 100 байтов (что гораздо больше того, что ожидаешь встретить в нормальном тексте), мы можем использовать формат %99s (оставляя один байт на заключительный ' \0'), что будет означать для fscanf приказ остановиться после 99 байт. Тогда длинное слово окажется разбитым на части, что некстати, но вполне безопасно. Мы могли бы написать

?enum { BUFSIZE = 100 };

?char fmtrj = "%99s"; /1 BUFSIZE-1 ./

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

Аргументы build — массив prefix, содержащий предыдущие NPREF введенных слов и указатель FILE. Массив prefix и копия введенного слова передаются в add, которая добавляет новый элемент в хэш-таблицу и обновляет префикс:

/* add: добавляет слово в список суффиксов, обновляет префикс */

void add(char *prefix[NPREF], char *suffix) {

State *sp;

sp = lookup(prefix, 1);

/* создать, если не найден

*/ addsuffix(sp, suffix); /*

сдвиг слов вниз в префиксе */ memmove(prefix, prefix+1, (NPREF-1)*sizeof(prefix[0])); prefix[NPREF-1] = suffix;

}

Вызов memmove — идиоматический способ удаления из массива. В префиксе элементы с 1 по NPREF-1 сдвигаются вниз на позиции с 0 по NPREF-2, удаляя первое слово и освобождая место для нового слова в конце.

Функция addsuf fix добавляет новый суффикс:

/* addsuffix: добавляет в состояние. */

/* Суффикс впоследствии не должен меняться */

void addsuffix

(State *sp, char *suffix)

{

Suffix *suf;

suf = (Suffix *) emalloc (sizeof(Suffix)); suf->word =

suffix; suf->next = sp->suf; sp->suf = suf;

}

Мы разделили процесс обновления состояния на две функции — add просто добавляет суффикс к префиксу, тогда как addsuffix выполняет тесно связанное с реализацией действие — добавляет слово в список суффиксов. Функция add вызывается из build, но addsuffix используется только изнутри add — это та часть кода, которая может быть впоследствии изменена, поэтому лучше выделить ее в отдельную функцию, даже если вызываться она будет лишь из одного места.