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

3.4. Генерация вывода

Теперь, когда структура данных построена, пора переходить к следу­ющему шагу — генерации нового текста. Основная идея остается неиз­менной: у нас есть префикс, мы случайным образом выбираем один из возможных для него суффиксов, печатаем его, затем обновляем пре­фикс. Это повторяющаяся часть обработки; нам еще надо продумать, как начинать и как заканчивать алгоритм. Начать будет нетрудно, если мы запомним слова первого префикса и начнем с них. Закончить алгоритм также нетрудно; для этого нам понадобится слово-маркер. Прочтя весь вводимый текст, мы можем добавить некий завершитель — "слово", ко­торое с гарантией не встретится ни в одном тексте:

build(prefix, stdin);

add(prefix, NONWORD);

В этом фрагменте NONWORD — некоторое значение, которое точно ни­когда не встретится в тексте. Поскольку по нашему определению сло­ва разделяются пробелами, на роль завершителя подойдет "слово", равносильное пробелу, но отличное от него, например символ перево­да строки:

char NONWORD[] = "\n"; /* никогда не встретится */

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

Можно начать процесс генерации с создания фиктивного префикса, который даст гарантию, что для работы программы всегда хватит вводи­мого текста. Для начала можно инициализировать все значения массива префиксов словом NONWORD. Это даст дополнительное преимущество — первое слово во вводимом файле будет первым суффиксом нашего вы­мышленного префикса, так что в генерирующем цикле печатать надо будет только суффиксы.

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

Добавление нескольких NONWORD в концы структур данных значительно упрощает основные циклы программы — это хороший пример использо­вания специальных значений для маркировки границ.— сигнальных меток (sentinel).

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

|Функция gene rate использует алгоритм, который мы только что опи­сали в общих словах. Она генерирует текст по слову в строке; эти слова можно группировать в более длинные строки при помощи любого тек­стового редактора — в главе 9 будет показано простое средство для тако­го форматирования — процедура fmt.

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

/* generate: генерирует вывод по одному слову в строке */

void generate(int nwords)

{

State *sp;

Suffix *suf;

char *prefix[NPREF], *w;

int i, nmatch;

for (i = 0; i < NPREF; i++) /* начальные префиксы */

prefix[i] = NONWORD;

for (i =0; i < nwords; i++) {

sp = lookup(prefix, 0);

nmatch = 0;

for (suf = sp->suf; suf != NULL; suf = suf->next)

if (rand() % ++nmatch == 0) /* prob = 1/nmatch */

w = suf->word;

if (strcmp(w, NONWORD) == 0)

break;

printf("%s\n", w);

memmove( prefix, prefix+1, (NPREF-1)*sizeof(prefix[0]));

prefix[NPREF-1] = w;

Обратите внимание на алгоритм случайного выбора элемента, ког­да число всех элементов нам неизвестно. В переменной nmatch подсчитывается количество совпадений при сканировании списка. Выражение

rand() % ++nmatch == 0

увеличивает nmatch и является истинным с вероятностью 1/nmatch. Та­ким образом, первый подходящий элемент будет выбран с вероятнос­тью 1, второй заменит его с вероятностью 1/2, третий заменит выбран­ный из предыдущих с вероятностью 1/3 и т. п. В каждый момент времени каждый из k просмотренных элементов будет выбран с вероят­ностью1/k.

Вначале мы устанавливаем prefix в стартовое значение, которое с га­рантией присутствует в хэш-таблице. Первые найденные значения Suffix будут первыми словами документа, поскольку только они следу­ют за стартовым префиксом. После этого суффикс выбирается случай­ным образом. В цикле вызывается lookup для поиска в хэш-таблице эле­мента (множества суффиксов), соответствующего данному префиксу; после этого случайным образом выбирается один из суффиксов, он печа­тается, а префикс обновляется.

Если выбранный суффикс оказывается NONWORD, то все заканчивается, поскольку это означает, что мы достигли состояния, относящегося к кон­цу введенного текста. Если суффикс не NONWORD, то мы его печатаем, а да­лее с помощью вызова memmove удаляем первое слово из префикса и делаем суффикс вторым словом нового префикса, после чего цикл повторяется. Теперь все написанное можно свести воедино в функцию main, кото­рая читает стандартный ввод и генерирует не более заданного количе­ства слов:

/* markov main: генерация случайного текста */

/* по алгоритму цепей Маркова */

int main(void)

{

int i, nwords = MAXGEN;

char *prefix[NPREF]; /* текущий вводимый префикс */

for (i = 0; i < NPREF; i++) /* начальный префикс */

prefix[i] = NONWORD;

build(prefix, stdin);

add(prefix, NONWORD);

generate(nwords);

return 0; ,

}

На этом разработка программы на С завершена. В конце главы мы сравним программы, написанные на разных языках. Главные достоин­ства С состоят в том, что он предоставляет программисту возможность полного управления реализацией и что программы, написанные на С, работают, как правило, весьма быстро. Расплачиваться за это приходит­ся тем, что программист вынужден выполнять большую работу: он сам должен выделять и освобождать память, создавать хэш-таблицы и связ­ные списки и т. п. С — инструмент с остротой бритвы: с его помощью можно создать и элегантную программу, и кровавое месиво.

Упражнение 3-1

Алгоритм случайного выбора элемента из списка неизвестной длины зависит от качества генератора случайных чисел. Спроектируйте и осу­ществите эксперименты для проверки метода на практике.

Упражнение 3-2

Если вводимые слова хранятся в отдельной хэш-таблице, то каждое слово окажется записанным лишь единожды, следовательно — эконо­мится место. Оцените, сколько именно — измерьте какие-нибудь фраг­менты текста. Подобная организация позволит нам сравнивать указате­ли, а не строки в хэш-цепочках префиксов, что выполняется быстрее. Реализуйте этот вариант и оцените изменения в быстродействии и раз­мере используемой памяти.

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

Удалите выражения, которые помещают сигнальные метки NONWORD в начало и конец данных, и измените generate так, чтобы она нормально запускалась и останавливалась без их использования. Убедитесь, что вывод корректен для О, 1, 2, 3 и 4 слов. Сравните код с использованием сигнальных меток и код без них.

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