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

3.5.Java

Вторую реализацию алгоритма markov мы создадим на языке Java. Объектно-ориентированные языки вроде Java заставляют нас обращать особое внимание на взаимодействие между компонентами программы. Эти компоненты инкапсулируются в независимые элементы данных, называемые объектами или классами; с ними ассоциированы функции, называемые методами.

Java имеет более богатую библиотеку, чем С. В частности, эта библио­тека включает в себя набор классов-контейнеров (container classes) для группировки существующих объектов различными способами. В каче­стве примера можно привести класс Vector, который представляет собой динамически растущий массив, где могут храниться любые объекты типа Object. Другой пример— класс Hashtable, с помощью которого можно сохранять и получать значения одного типа, используя объекты другого типа в качестве ключей.

В нашем приложении экземпляры класса Vector со строками в ка­честве объектов — самый естественный способ хранения префиксов и суф­фиксов. Так же естественно использовать и класс Hashtable, ключами в котором будут векторы префиксов, а значениями — векторы суффик­сов. Конструкции подобного рода называются отображениями (mар) префиксов на суффиксы; в Java нам не потребуется в явном виде зада­вать тип State, поскольку Hashtable неявным образом сопоставляет пре­фиксы и суффиксы. Этот дизайн отличается от версии С, где мы созда­вали структуры State, в которых соединялись префиксы и списки суф­фиксов, а для получения структуры State использовали хэширование префикса.

Hashtable предоставляет в наше распоряжение метод put для хране­ния пар ключ-значение и метод get для получения значения по задан­ному ключу:

Hashtable h = new Hashtable();

h.put(key, value);

Sometype v = (Sometype) h.get(key);

В нашей реализации будут три класса. Первый класс, Prefix, содер­жит слова префиксов:

class Prefix {

public Vector pref; // NPREF смежных слов из ввода

……..

Второй класс, Chain, считывает ввод, строит хэш-таблицу и генериру­ет вывод; переменные класса выглядят так:

class Chain {

static final int NPREF = 2; // размер префикса

static final String NONWORD = "\n";

// "слово", которое не может встретиться в тексте

Hashtable statetab = new Hashtable();

//ключ = Prefix, значение = suffix Vector

Prefix prefix = new Prefix(NPREF, NONWORD);

// начальный префикс

Random rand = new Random();

……

Третий класс — общедоступный интерфейс; в нем содержится функция main и происходит начальная инициализация класса Chain:

class Markov {

static final int MAXGEN = 10000; // максимальное количество

// генерируемых слов

public static void main(String[] args) throws I0Exception

{

Chain chain = new Chain();

int nwords = MAXGEN;

chain.build(System.in);

chain.generate(nwords);

}

}

После того как создан экземпляр класса Chain, он в свою очередь создает хэш-таблицу и устанавливает начальное значение префикса, соcтоящее из NPREF-констант NONWORD. Функция build использует библио-Жтечную функцию StreamTokenizer для разбора вводимого текста на I слова, разделенные пробелами. Первые три вызова перед основным циклом устанавливают значения этой функции, соответствующие нашему определению термина "слово":

// Chain build: создает таблицу состояний из потока ввода

void build(InputStream in) throwsI0Exception

{

StreamTokenizer st = new StreamTokenizer(in);

st.resetSyntax(); // удаляются правила по умолчанию

st.wordChars(0, Character.MAX_VALUE); // включаются все st.whitespaceChars(0, ' '); //литеры, кроме пробелов while (st.nextToken() != st.TT_EOF)

add(st.sval);

add(NONWORD);

}

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

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

обновляет префикс

void add(String word)

{

Vector suf = (Vector) statetab.get(prefix);

if (suf == null) {

suf = new Vector();

statetab.put(new Prefix(prefix), suf);

}

suf.addElement(word);

prefix.pref.removeElementAt(0);

prefix.pref.addElement(word);

}

Обратите внимание на то, что если suf равен null, то add добавляет в хэш-таблицу префикс как новый объект класса Pref ix, а не собственно pref ix. Это сделано потому, что класс Hashtable хранит объекты по ссылкам, и если мы не сделаем копию, то можем перезаписать данные в таблице. Собственно говоря, с этой проблемой мы уже встречались при написа­нии программы на С.

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

// Chain generate: генерирует выходной текст

void generate(int nwords)

{

prefix = new Prefix(NPREF, NONWORD);

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

Vector s = (Vector) statetab.get(prefix);

int r = Math. abs( rand. Nextlnt() ) % s.size();

String suf = (String) s.elementAt(r);

if (suf.equals(NONWORD))

break;

System.out.println(suf);

prefix.pref. removeElementAt(0);

prefix.pref.addElement(suf);

}

}

Два конструктора Prefix создают новые экземпляры класса в зависи­мости от передаваемых параметров. В первом случае копируется суще­ствующее значение типа Prefix, а во втором префикс создается из п ко­пий строки; этот конструктор используется для создания NPREF копий NONWORD при инициализации префикса:

// конструктор Prefix: создает копию существующего префикса

Prefix(prefix p)

{

pref = (Vector) p.pref.clone();

}

// конструктор Prefix: n копий строки str

Prefix(int n, String str)

{

pref = new Vector ();

for (int i=0; i<n; i++ )

pref.addElement(str);

}

Класс Prefix имеет также Два метода, hashCode и equals, которые неяв­но вызываются из Hashtable для индексации и поиска по таблице. Нам пришлось сделать Prefix полноценным классом как раз из-за этих двух методов, которых требует Hashtable, иначе для него можно было бы ис­пользовать Vector, как мы сделали с суффиксом.

Метод hashCode создает отдельно взятое хэш-значение, комбинируя набор значений hashCode для элементов вектора:

static final int MULTIPLIER = 31; // для hashCode()

// Prefix hashCode: генерирует хэш-значение

// на основе всех слов префикса

public int hashCode()

{

int h = 0;

for (int i = 0; i < pref.size(); i++)

h = MULTIPLIER * h + pref.elementAt(i).hashCode();

return h;

}

Метод equals осуществляет поэлементное сравнение слов в двух пре­фиксах:

// Prefix equals: сравнивает два префикса на идентичность слов

public boolean equals(0bject о)

{

Prefix p = (Prefix) о;

for (int i = 0; i < pref.size(); i++)

if (!pref.elementAt(i).equals(p.pref.elementAt(i)))

return false;

return true;

}

Программа на Java гораздо меньше, чем ее аналог на С, при этом боль­ше деталей проработано в самом языке — очевидными примерами явля­ются классы Vector и Hashtable. В общем и целом управление хранением данных получилось более простым, поскольку вектора растут, когда нужно, а сборщик мусора (garbage collector — специальный автомати­ческий механизм виртуальной машины Java) сам заботится об освобож­дении неиспользуемой памяти. Однако для того, чтобы использовать класс Hashtable, нам пришлось-таки самим писать функции hashCode и equals, так что нельзя сказать, что язык Java заботился бы обо всех деталях.

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

Пропустив эту программу с исходным (химическим) текстом и форма­тируя сгенерированный текст с помощью процедуры fmt, мы получили следующее:

% Java Markov <j r_chemistry.txt | fmt

Wash the blackboard. Watch it dry. The water goes

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