Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Бичков - Основи сучасного програмування.doc
Скачиваний:
69
Добавлен:
07.03.2016
Размер:
2.67 Mб
Скачать

Автоматна технологія програмування

Основи сучасного програмування Основи сучасного програмування Основи сучасного програмування Основи сучасного програмування Основи сучасного програмування Основи сучасного

Найважливішими технологіями останніх десятиріч є методології структурного та об'єктно-орієнтованого програмування. Паралельно з ними використовується й розвивається метод програмування, заснований на скінченних автоматах (СА). Легко можна переконатися, що ООП успадковувало у СА такі позиції, як об'єкт і виконання функцій, властивих даному стану об'єкта.

Ми розглядатимемо не загальну теорію СА (хоча й вивчимо деякі властивості й визначення), а "автоматний" стиль мислення для розв’язання задач певного класу.

Модель СА – така сама алгоритмічна модель, як і блок-схема. Проте автомат має якості, яких у ній немає. Принциповою суттю СА є наявність станів, на основі яких відбуваються всі дискретні перетворення.

Скінченний автомат як формалізм. Роботу лексичного аналізатора краще описувати формалізмом СА.

Означення 9.1. СА – це п'ятірка (К, VT, F, H, S), де

К скінченна множина станів; VT скінченна множина допустимих вхідних символів; F відображення множини K × VT K, що визначає поведінку автомата; відображення F часто називають функцією переходів; HК – початковий стан; SK – завершальний стан (або скінченна SK завершальних станів). F(А, t)  B означає, що зі стану А по вхідному символу t відбувається перехід у стан B.

У кожен поточний момент часу СА може перебувати в одному з можливих станів (кількість станів, у яких може знаходитися СА, скінченна). Автомат послідовно прочитує символи вхідного тексту (рядки). Кожен прочитаний символ або переводить автомат у новий стан, або залишає його в поточному. Формально автомат можна описати за допомогою функції переходів. Її аргументами є попередній стан автомата і поточний прочитаний символ, а значенням – новий стан автомата.

СА зручно задавати діаграмою його переходів або станів. Діаграма є орієнтованим графом, вершинами якого є однойменні стани автомата; дуга з вершини qi у вершину qk з надписаною над нею літерою аj проводиться тоді й тільки тоді, коли F(qi,аj) qk, тобто коли автомат зі стану qi під впливом аj має перейти в стан qk. У разі, коли перехід з qi у qk здійснюється під впливом будь-якої з літер деякої підмножини S, SА, усі літери цієї підмножини підписуються над відповідною дугою. Якщо довільний стан qi входить у S, то даний факт на діаграмі наголошується жирним кружком, що виділяє вершину qi. Це означає, що автомат перебуває в одному зі своїх завершальних станів.

Очевидно, що будь-який СА повністю визначається своєю діаграмою переходів. Під задачею побудови СА, що має ті чи інші властивості, розумітимемо задачу побудови діаграми його станів.

Основною властивістю стану є те, що в ньому СА чекає обмежений набір вхідних дій. Будь-який алгоритм має вхідну й вихідну інформацію. Її можна розділити на два типи: змінні (напр., операції над властивостями об'єктів) і функції. Друга властивість стану – видача набору фіксованих значень вихідних змінних. Це означає, що в будь-який момент часу визначені значення всіх вихідних змінних, оскільки алгоритм завжди перебуває в якомусь стані.

На рис. 9.1. подано діаграму автомата К, що працює зі словами алфавіту A  {а, b, c}. Автомат має два стани, q0 і q1, завершальним є стан q1. Почавши роботу в стані q0, автомат при отриманні на вхід літер а, b у новий стан не переходить; при отриманні літери с здійснюється перехід у стан q1; далі

Рис. 9.1

наступна літера залишає автомат у тому самому стані. Таким чином, автомат K розпізнає мову L1, що складається зі слів, які мають у своєму складі літеру с, тому запропонована мова є регулярною.

Природно, що при виконанні дій можливі виклики вкладених СА.

Кількість станів і наборів значень вихідних змінних обмежена. Діаграми станів, що ілюструють роботу СА, стали невід'ємною частиною основних принципів об'єктно-орієнтованого програмування.

Основною особливістю програмної реалізації СА є обов'язкова наявність циклу. На відміну від традиційних алгоритмів, що включають два типи компонент – умову й дію, – cкінченний автомат має додаткову компоненту – стан.

Основи конструювання програм за допомогою СА. Одним з інструментів розгляду рядків символів є СА. Зазвичай вони застосуються для розбиття початкового рядка на набір деяких лексичних одиниць.

Нехай дано два масиви, що закінчуються нулем. Другий відрізняється від першого додатковими вставками з чисел. Наприклад:

M = 1 2 3 4 5 6 7 8 9 0

N = 1 2 3 4 5 3 2 4 43 6 7 8 9 0

Програма має порівняти два масиви й надрукувати вставки, що зустрічаються.

Масиви позначимо M і N, а номери їх елементів – відповідно i і j.

Необхідно написати програму знаходження й друку вставок.

Побудуємо діаграму станів. Позначимо початковий стан як А. Перебиратимемо елементи масиву, використовуючи цикл (NonStop=true). Вихід із циклу (NonStop=false) обробки масивів здійснюється після закінчення елементів або в обох масивах, або в одному. Із початкового стану здійснюється перехід до поки що невідомого стану й виконання певних дій. Цей етап подано на рис. 9.2.

Рис. 9.2

Розглянемо новий стан В. У ньому порівнюватимемо елементи двох масивів. Поки порівнювані елементи рівні – послідовності збігаються. Тому стан В назвемо станом очікування появи вставки, або нерівності поточних елементів масивів. У цьому стані виконуватиметься приріст номерів елементів масивів на одиницю (i=i+1;j=j+1). Оскільки робота починається з першого елемента масивів, то i=1; j=1. Це перша дія при виході з початкового стану.

У стані В порівнюватимемо елементи M[i] і N[j]. У разі їх нерівності (локальна подія M[i]<>N[j]) переходимо в невідомий поки стан (позначимо його С) із запам'ятовуванням поточних номерів елементів масивів у додаткових змінних i1=i;j1=j (рис. 9.3).

У стані В може закінчитися список елементів масивів – M[i]=0 і N[j]=0. Якщо досягнутий кінець другого масиву й не досягнутий першого (подія EndN, або N[j]=0), то формується повідомлення про помилку і здійснюється вихід. Для цього вводиться перехід зі стану В у стан А за локальною подією ЕndN з дією на переході Write('Error') і NonStop=false, де Write('Error')друк повідомлення про помилку. Якщо обидва чергові елементи масивів нульові, то відбувається локальна подія ЕndМ&EndN і програма переходить у стан А з дією коректного завершення NonStop=false.

Рис. 9.3

Таким чином, у стані В вибираємо чергову пару елементів порівнюваних масивів і чекаємо появи однієї з трьох локальних подій:

1. EndM & not EndN – закінчилися елементи в другому масиві й не закінчилися в першому.

2. EndM & EndN – закінчилися елементи в обох масивах.

3. M[i]<>N[j] – знайшли незбіжні елементи.

За кожною із цих подій здійснюється перехід зі стану В або до стану А – за подіями 1 і 2, або до невідомого поки що стану С – за подією 3. Якщо жодна з цих подій не трапилася в поточному циклі, то програма залишається в стані В, що відображається дугою з В у В.

Розглянемо події, які відбуватимуться в стані С. У цьому стані відбувається очікування появи елемента N[j1], збіжного з M[i1]. Настання цієї події свідчить про завершення вставки. У стані С необхідно здійснити приріст індексу j1 елемента другого масиву і друк відповідного елемента (рис. 9.4).

Іншою локальною подією є кінець послідовності другого масиву, тобто ЕndN(N[j1]=0). За цією подією здійснюється перехід у початковий стан А і формується код виходу з циклу NonStop=false.

Поки не будуть знайдені збіжні елементи, знаходимося в стані С. Задачу розв'язано.

Рис. 9.4

Введемо такі означення для реалізації СА у вигляді програми.

Означення 9.2. Говоритимемо, що локальна подія відбулася, якщо маємо позитивний результат деякого логічного виразу.

Якщо локальних подій у програмі більше одної, то кожній з них ставиться у відповідність свій логічний вираз.

Пронумеруємо стани СА (за допомогою літер або цифр). Тоді локальні події позначимо двома індексами, перший з яких позначає поточний стан, а другий – стан, у який має перейти програма в наступному циклі при настанні даної локальної події.

Означення 9.3. Стан є фрагментом програми, у якому очікується локальна подія за наслідками обчислень логічних виразів, що входять у даний фрагмент програми.

Якщо жодна локальна подія при поточному проходженні циклу не відбулася, то програма залишається в тому самому стані, тобто обчислює той самий фрагмент і в наступному проходженні циклу. Якщо локальна подія настала, то до наступного проходження циклу програма переходить у новий стан – до іншого фрагмента, де мають очікуватися інші локальні події.

Іншими словами: стан програми, що реалізує СА – це зациклення на одному й тому самому фрагменті програми до настання локальної події. Позначимо стан як where.

Означення 9.4. Переходом назвемо зміну поточного стану на інший.

При цьому змінна стану where змінює своє значення.

Кожен перехід супроводжується деякою дією. Це означає, що при настанні локальної події потрібно не тільки змінити змінну where, але й виконати задану послідовність операторів. Тому загальний вигляд розв'язання задачі за допомогою СА такий:

int NonStop=1;

while(NonStop)

{

//тіло автомата;

}

де NonStop ознака продовження циклу, який перед передаванням керування оператору while має бути встановлений на значення true.

Уся логіка роботи програми полягає в циклічній обробці станів. Вихід із циклу здійснюється за виконання умови закінчення обробки. У тілі циклу відбувається обробка кожного зі станів. СА може бути реалізований так:

While (NonStop)

switch(where)//Змінна стану автомата.

{

case А:

//перевірка умов на дугах і петлях і;

//виконання переходів і дій

break;

...

case Z:

//перевірка умов на дугах і петлях і;

//виконання переходів і дій

break;

};

Розглянемо процес програмної реалізації СА детальніше.

Розв'яжемо задачу про видалення з тексту коментаря програми, що написана мовою Паскаль. Нагадаємо, що коментарем називається послідовність символів, вміщена в спеціальні дужки: (*…*).

Видалення коментаря з тексту проводиться шляхом збереження основного тексту й незбереження тексту коментаря. Символи читаються й записуються, поки не зустрінуть початок коментаря. Потім початок коментаря та його символи просто прочитуються без збереження. Після досягнення закінчення коментаря знову читаємо та зберігаємо символи. Розберемо цей процес детальніше.

Якщо поточним прочитаним символом є "(", то далі, можливо, ітимуть символи, що утворюватимуть коментар, тобто можливий перехід у стан початку коментаря. Це залежить від наступного прочитаного символу. Якщо прочитаний символ "*", то "(" і "*" не зберігаються, і процес читання продовжується без збереження поточних символів. Інакше "(" зберігаємо й читаємо (зі збереженням) символи далі. Читання триває до появи символу "*". Це означає, що, можливо, далі йтиме стан закінчення коментаря. Якщо наступний прочитаний символ ")", то коментар закінчений, інакше продовжуємо читати й не зберігати прочитаний текст.

Можна виділити стани, у яких перебуває наша програма.

Стан "поза коментарем". У цьому стані читаємо й записуємо символи тексту.

Стан можливого початку коментаря. Прочитано символ "(". При читанні наступного символу можливі такі ситуації: поточним є прочитаний символ "*". У цьому випадку "(" і "*" не зберігаються, і програма переходить у стан "усередині коментаря"; поточним є будь-який інший символ. У цьому випадку зберігаємо символ "(" і переходимо в стан "поза коментарем".

Стан "усередині коментаря". Якщо прочитано поточний символ "*", то переходимо в стан можливого закінчення коментаря, інакше залишаємося в початковому стані, тобто продовжуємо читати без збереження тексту.

Стан можливого закінчення коментаря. Прочитано символ "*". При читанні наступного символу можливі такі ситуації: поточним є прочитаний символ "*". У цьому випадку необхідно зберегти попередній символ і залишитися в стані можливого закінчення коментаря; поточним є будь-який інший символ. У цьому випадку здійснюємо перехід у стан "усередині коментаря".

Складемо діаграму станів (рис. 9.5).

Рис. 9.5

Позначимо через а читання будь-якого символу, окрім "("; b –будь-якого символу, окрім "(" і "*"; d – будь-якого символу, окрім "*" і ")"; А – стан "поза коментарем"; B – стан можливого початку коментаря; С – стан "усередині коментаря"; D – стан можливого кінця коментаря.

Деталізуємо автоматний алгоритм:

static char where='A';

int NonStop=1;

while(NonStop)

{

switch(where)

{

case 'A':

//обробка стану А;

break;

case 'B':

//обробка стану B;

break;

case 'C':

//обробка стану З;

break;

case 'D':

//обробка стану D;

break;

}

}

Локальній події взаємно однозначно відповідає перехід з поточного стану в інший. Якщо жодна з обчислюваних у даному стані локальних подій не настала, то зберігається поточний стан. Цьому відповідає логічна умова, що заперечує умову переходів в інші стани.

За локальною подією програма переходить у наступному циклі в новий або залишається в старому стані. Однак перш ніж перейти в новий стан, вона має здійснити деяку дію на переході.

Правильна програма, що реалізовує СА, має містити дію закінчення циклу, щоб уникнути зациклення в одному стані.

Введемо такі позначення:

 поза коментарем – out;

 можливий початок коментаря – beg_com;

 усередині коментаря – in_com;

 можливий кінець коментаря – end_com.

При читанні першого символу СА знаходиться в початковому стані – where=out. Подальші дії здійснюються залежно від значення where. Наприклад, якщо where=out і current='/', то переходимо в стан "початок коментаря" – змінюється стан СА, тобто треба змінній where надати значення beg_com. Якщо читається будь-який інший символ, то необхідно його записати у вихідний файл, і СА залишиться в поточ­ному стані.

out:switch(current)

{

case '/':where=beg_com;break;

default:putc(current,f_out);

}

Нехай, наприклад, СА перебуває у стані "початок коментаря". Якщо поточний прочитаний символ '/', то записуємо його у вихідний файл і не змінюємо стан СА; якщо поточний символ '*', то переходимо в стан "усередині коментаря", і where=in_com. При читанні будь-якого іншого символу друкуємо його у вихідний файл і переходимо у стан "поза коментарем" where=out.

beg_com:switch(current)

{

case'/':putc(current,f_out);break;

case'*':where=in_com;break;

default:

putc('/',f_out);

putc(current,f_out);

where=out;

}

Аналогічно програмуються всі інші стани СА.

Тепер легко можна написати програму, що реалізує СА, наведений на рис. 9.5:

#include <stdio.h>

enum states{out,beg_com, in_com, end_com};

char current;

states where;

FILE*f_in,*f_out;

main(){

where=out;

f_in=fopen("c:\\text_in.txt","r");

f_out=fopen("c:\\text_out.txt","w");

while(!feof(f_in))

{

current=getc(f_in);

switch(where) {

case out:

switch(current)

{

case '/':where=beg_com;break;

default:putc(current,f_out);

}

break;

case beg_com:

switch(current)

{

case'/':putc(current,f_out);break;

case'*':where=in_com;break;

default:

putc('/',f_out);

putc(current,f_out);

where=out;

}

break;

case in_com:switch(current){

case'*':where=end_com;

}

case end_com:switch(current)

{

case'*':;

case'/':where=out;

default:where=in_com;

}

}

}

fclose(f_in);

fclose(f_out);

}

Розглянемо ще кілька прикладів.

1. Нехай необхідно зі вхідного файла виділити ідентифікатори, що містять цифри. Припустимо для простоти, що синтаксично ідентифікатори записані правильно.

Для класифікації послідовностей символів визначимо такі множини:

L – 'A'..'Z', 'a'..'z', '_';

D – '0'..'9';

Rроздільники ' ', '.', ',', ':', ';', '-', '+', EOL;

Визначимо стани, у яких знаходитиметься СА:

I – очікування появи ідентифікатора;

L – обробка ідентифікатора. Очікування появи цифри;

LD – обробка ідентифікатора з цифрою. Очікування кінця ідентифікатора.

Автомат має виконувати такі дії:

M – пропустити символ;

А – записати символ у буфер;

W – вивести буфер у вихідний файл і очистити;

С – очистити буфер.

Складемо діаграму станів СА (рис. 9.6).

Рис. 9.6

2. Нехай маємо послідовність символів x1...xn. Визначити, чи є в ній символи "abcd", що йдуть один за одним. Іншими словами, вимагається з'ясувати, чи міститься в слові Х підслово "abcd".

Проглядатимемо слово Х зліва направо в очікуванні появи символу 'a'. Як тільки він з'явиться, чекаємо появи за ним символу 'b', потім 'c' і, нарешті, 'd'.

Таким чином, ми в кожен момент знаходимося в одному з таких станів: початковий (0), очікування b після а (A), очікування с після ab (B), очікування d після abc (C) і вихід після abcd (D).

Складемо діаграму станів (рис. 9.7).

Рис. 9.7

Стан Exit означає закінчення роботи. Напишемо відповідну програму:

enum states{A,B,C,D};

enum boolean{false,true};

char current;

boolean stop;

states where;

FILE*f_in;

main(){

where=A;

stop=false;

f_in=fopen("c:\\text_in.txt");

while(!feof(f)||stop)

{

current=getc(f_in);

switch (where)

{

case A:switch(current)

{

case'a':where=B;break;

default:;

}

break;

case B:switch(current)

{

case'a':;break;

case'b':where=C;break;

default:where=A;

}

break;

case C:switch(current)

{

case'c':where=D;break;

case'a':where=B;break;

default:where=A;

}

break;

case D:switch(current)

{

case'd':

printf("входить\n");

stop=true;

break;

case'a':where=B;break;

default:where=A;

}

}

}