Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

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

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

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[NPREF]; /* слова префикса */

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;

h = 0;

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

for (p = (unsigned char *) s[i]; *p != '\0'; p++)

h = MULTIPLIER * h + *p;

return h % NHASH;

}

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

/* lookup: ищет префикс; создает его при необходимости. */

/* возвращает указатель, если префикс существует или создан; */

/* NULL в противном случае. */

/* при создании не делает strdup, так что строки

/* не должны изменяться после помещения в таблицу. */

State* lookup(char *prefix[NPREF], int create)

{

int i, h;

State *sp;

h = hash(prefix);

for (sp = statetab[h]; 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;

}

if (create) {

sp = (State *) emalloc(sizeof(State));

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

sp->pref[i] = prefixfi];

sp->suf = NULL;

sp->next = statetab[h];

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)-l);

while (fscanf(f, fmt, buf) != EOF)

add(prefix, estrdup(buf));

}

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

? enum { BUFSIZE = 100 };

? char fmt[] = "%99s"; /* 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 f ix добавляет новый суффикс:

/* 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 — это та часть кода, которая может быть впо­следствии изменена, поэтому лучше выделить ее в отдельную функцию, даже если вызываться она будет лишь из одного места.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]