Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Зубенко, Омельчук - Програмування. Поглиблений курс

.pdf
Скачиваний:
50
Добавлен:
07.03.2016
Размер:
4.72 Mб
Скачать

Розділ IV. АЛГОРИТМИ

while((op=*token)=='*'||op=='/'||op=='%') { getToken();

evalExp3(&temp); switch(op) {

case '*': *answer=*answer*temp; break;

case'/': if(temp==0.0) {

serror(3); /*ділення на нуль*/ *answer=0.0;

} else *answer=*answer/temp; break;

case'%':

*answer=(int) *answer % static_cast<int> (temp); break;

}

}

}

/*Піднесення до степеня*/

void CSimpleParser::evalExp3(double *answer)

{

double temp, ex; register int t;

evalExp4(answer); if(*token=='^') { getToken();

evalExp3(&temp);

ex=*answer; if(temp==0.0) { *answer=1.0; return;

}

for(t=temp-1; t>0; --t) *answer=(*answer)* static_cast<double>(ex);

}

}

/*Множення унарних операцій + та -*/

void CSimpleParser::evalExp4(double *answer)

{

register char op=0;

if((tokType==DELIMITER) && *token=='+'||*token=='-') { op=*token;

getToken();

}

evalExp5(answer);

561

ПРОГРАМУВАННЯ

if(op=='-') *answer=-(*answer);

}

/*Обчислення виразів у дужках*/

void CSimpleParser::evalExp5(double *answer)

{

if((*token=='(')) { getToken(); evalExp1(answer); if(*token!=')') serror(1); getToken();

}

else atom(answer);

}

/*Одержання значення числа або змінної*/ void CSimpleParser::atom(double *answer)

{

switch(tokType) { case VARIABLE:

*answer=findVar(token);

getToken();

return; case NUMBER:

*answer=atof(token);

getToken();

return;

default:

serror(0);

}

}

void CSimpleParser::putback()

{

char *t;

t=token;

for(; *t; t++) prog--;

}

/*Відображення повідомлення про помилку*/ void CSimpleParser::serror(int error)

{

static char *e[]={ "Синтаксична помилка", "Незбалансовані дужки",

562

Розділ IV. АЛГОРИТМИ

"Немає виразу", "Ділення на нуль"

};

printf("%s\n", e[error]);

}

/*Повернення чергової лексеми*/ void CSimpleParser::getToken()

{

register char *temp='\0';

temp=token;

tokType=0;

if(!*prog) return; /*кінець виразу*/ while(isspace(*prog)) ++prog; /*пропустити пробіли,

символи табуляції й порожнього рядка*/

if(strchr("+-*/%^=()", *prog)){ tokType=DELIMITER;

/*перейти до наступного символу*/ *temp++=*prog++;

}

else if(isalpha(*prog)) { while(!isdelim(*prog)) *temp++=*prog++; tokType=VARIABLE;

}

else if(isdigit(*prog)) { while(!isdelim(*prog)) *temp++=*prog++; tokType=NUMBER;

}

*temp='\0';

}

/*Повернення значення ІСТИНА, якщо с є роздільником*/ int CSimpleParser::isdelim(char c)

{

if(strchr("+-/*%^=()", c)||c==9||c=='\r'||c==0) return 1;

return 0;

}

У наведеному вигляді програма підтримує такі операції: +, -, *, /, %. Крім того, вона вміє підносити до цілочислового степеня (^) і обчислювати унарний мінус, а також коректно розпізнавати дужки.

563

ПРОГРАМУВАННЯ

Програма складається з шести рівнів, а також методу atom, що повер- тає значення числа. Як обговорювалося раніше, у змінній token пове- ртається чергова лексема з рядка, що містить вираз, а в tokType тип лексеми. Атрибут prog указує на рядок, що містить вираз

Приклад 4.34. Застосування програми:

Лістинг.

int main()

{

double answer;

CSimpleParser *csp=new CSimpleParser;

/*Обробка виразів до введення порожнього рядка*/ int fl=1;

do {

cout<<"Введіть вираз:"; gets(csp->prog);

if(!*csp->prog) fl=0; else

{

csp->evalExp(&answer); cout<<"Результат:"<<answer<<endl;

};

} while(fl==1); delete csp; return 0;

}

Парсер дозволяє вводити вирази, подібні таким:

A =10/4;

A – B;

C =A * (F – 21)

При вивченні коду парсерів розглядався метод serror(), що ви- кликається в певних ситуаціях і повідомляє про помилки. На відміну від багатьох інших типів парсерів, рекурсивний спуск полегшує пе- ревірку синтаксису, оскільки в більшості випадків вона відбувається в методах atom(), findVar() та evalExp5(), де перевіряється правиль- ність розміщення дужок. Єдина проблема, пов'язана із синтаксични- ми помилками, полягає в тому, що при виявленні помилки аналіз ви- разу не припиняється. Це може привести до виведення кількох повід- омлень про помилки.

564

Розділ IV. АЛГОРИТМИ

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

*Література для CР: загальні методи синтаксичного аналізу – [7, 11, 65]; синтаксичний аналіз без повернення – [7, 11, 19, 65, 104]; висхідний синтаксичний аналіз – [7, 11, 65]; утиліти Lex та

Yacc – [143, 144].

Контрольні запитання та вправи

1.Що таке лексичний аналіз?

2.Що таке синтаксичний аналізатор?

3.Що таке синтаксичне дерево?

4.Що таке таблиця ідентифікаторів?

5.Що таке метод рекурсивного спуску з поверненням?

6.Що таке утиліти Lex та Yacc?

7. Нехай

задано

автомат:

Q = {q0,q1,q*};

F = {q1,q*};

T ={0,1,2,3,4,5,6,7,8,9}; q01 q1 ; q0 2 q1 ;

q0 3 q1 ;…;

q0 9 q1 ; q11 q1 ; q12 q1 ; q13 q1 ; …; q19 q1 . Побудувати за ним програму лексичного й синтаксичного аналізаторів.

8. Переписати програму з прикл. 4.33 так, щоб вона аналізува- ла й обчислювала арифметичний вираз з дійсними одноліте- рними змінними й однолітерними змінними-константами обох регістрів.

9.Те саме, що й у вправі 8, але іменами змінних і констант є довільні ідентифікатори. Передбачити розширений контроль над помилками.

10 * . Розробити стратегію і провести модульне та інтеграційне тестування аналізаторів із вправ 8 та 9 – спочатку для окре- мих функцій, потім усієї програми.

11.Реалізувати алгоритм усунення з правил КВ-граматики недо- сяжних символів (див. вправу 49 із підрозд. 1.4).

12.Реалізувати алгоритм пошуку множини Firstk (A) для нетер-

міналів граматики. Вивести множини Firstk (A) для кожного нетермінала.

13.Реалізувати алгоритм перевірки, чи буде задана КВ- граматика LL(k) граматикою для даного k .

14.Побудувати скінченний автомат і реалізувати лексичний аналі- затор для фрагмента мови С. Вивести вміст таблиці лексем піс-

565

ПРОГРАМУВАННЯ

ля обробки тексту програми або повідомлення про лексичні по- милки. Типи лексем визначити самостійно. Це можуть бути:

-зарезервовані слова (int, if, else, while, switch, case

тощо);

-ідентифікатори;

-числові константи (цілі й дійсні числа);

-літерні константи;

-коди операцій (+, -, *, =, ==, <,> тощо);

-роздільники ({, ,, ;, } тощо).

Підмножина мови С містить:

a) дані типу int, описи змінних, оператори присвоюван- ня в довільній послідовності; операції +, , *, = =, !=, <, >;

б) те саме, що й у а), але з доданими операторами if та if-else довільної вкладеності й у довільній послідовності;

в) те саме, що й у б), тільки оператори if та if-else за- мінити на оператори while;

г) те саме, що й у б), тільки оператори if та if-else за- мінити на оператори for;

д) те саме, що й у б), тільки оператори if та if-else за- мінити на оператори do-while;

е) те саме, що й у а), але з доданими типами float і ма- сивами зазначених типів.

15. Реалізувати синтаксичний аналізатор для описаних у впра- ві 14 фрагментів мови С методом рекурсивного спуску.

16. Побудувати синтаксичний аналізатор для даного поняття й там, де це можливо, обчислити значення заданого виразу. Вхідний потік вводиться з клавіатури. Поняття задано в лінійній СД (жирні літери-дужки є метасимволами, див. підрозд. 1.4.4):

1)<список-списків> ::= <список> {; <список> } <список> ::= <літера> {, <літера> }

2)<дійсне-число> ::= <ціле-число> . <ціле-без-знака> |

<ціле-число> [. <ціле-без-знака> ] Е<ціле-число> <ціле-без-знака> ::= <цифра>{<цифра> }

<ціле-число>

::= [- | + ]

<ціле-без-знака>

3) <сума>

::= <ціле> {<знак-операцїї> <ціле> }

<знак-операції>

::= - | + | *

<ціле>

::= <цифра> { <цифра> }

4) <дужки>

 

::= <квадратні> | <круглі>

<квадратні> ::= В | [<круглі> <круглі>]

<круглі>

::= А | (<квадратні> <квадратні>)

5) <простий-вираз>

::= <простий-ідентифікатор> |

(<простий-вираз> <знак-операції> <простий-вираз> )

<знак-операції>

::= - | + | *

 

<простий-ідентифікатор> ::= <літера>

566

 

 

Розділ IV. АЛГОРИТМИ

6) <список-параметрів> ::=

<параметр> {, <параметр>}

<параметр>

::= <ім'я> = <цифра> <цифра> |

<ім'я> = (<список-параметрів>))

 

<ім'я>

::= <літера> <літера> <літера>

7) <дужки>

::= <квадратні> | <круглі>

<квадратні> ::= В | [ [<квадратні> ] (<круглі> ) ]

<круглі> ::= А

| ((<круглі>) [<квадратні> ])

8) <константний-вираз>

::= <цифра> {<цифра>} |

(<константний-вираз> <знак-операції> <константний-вираз> )

<знак-операції>

 

::= - | + | *.

9) <простий-логічний>

::= <простий-ідентифікатор> |

TRUE | FALSE

| (<простий-логічний> <знак-операції>

<простий-логічний> )

::= <AND> | OR

<знак-операції>

 

<простий-ідентифікатор> ::= <літера>

10)Визначити, чи еквівалентний заданий простий логічний вираз виразу FALSE.

11)Визначити, чи зберігає заданий простий логічний вираз свої значення при довільній перестановці значень аргументів.

12)Визначити, чи еквівалентні два прості логічні вирази.

4.6. Пошук у графах

¾Зображення графів

¾Пошук шляхів у графах

¾Пошук у ширину

¾Пошук у глибину

¾Алгоритм Дейкстри

¾Застосування алгоритмів пошуку

¾Жадібні алгоритми

Ключові слова: комбінаторний вибух, зображення графа у вигляді списку су- міжних вершин і матриці суміжності, пошук у глибину, пошук у ширину, алгоритм Дейкстри.

Багато комбінаторних задач прямо чи опосередковано використо- вують графи. Більшість із них зводиться до задач пошуку в графах. Типовою є така задача. Нехай необхідно доїхати від Києва до Сімфе- рополя автобусом певної компанії ABC за умови, що між цими міста- ми немає прямого рейсу.

567

ПРОГРАМУВАННЯ

Цілком природно зобразити всі рейси компанії у вигляді зваженого графа, вершини якого відповідають містам, а дуги відстані між ни- ми (рис. 4.7). Задача зводиться до пошуку відповідного шляху в цьо- му графі (бажано якомого коротшого).

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

варіантів маршрутів має порядокO(nk ) .

Простим прикладом, що описує дану проблему, є швидкість збіль- шення кількості перестановок із n елементів при зростанні n .

Як відомо, кількість перестановок із n елементів дорівнює n !. Звідси кількість перестановок чотирьох елементів дорівнює 4! , тобто 24 , п'яти – 120 , а шести уже 720 . Кількість перестановок 1000 елементів становить

1000! = 4,02387260077093773543702433923e +2567 .

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

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

Усі ці методи пов'язані з різними стратегіями обходу графів. Однак зупинимося спочатку на основних способах зображення графів.

4.6.1. ЗОБРАЖЕННЯ ГРАФІВ

Теоретичні відомості про графи були розглянуті в підрозд. 1.4.4. Існують два стандартні способи зображення графа G = (V ,E ) у про- грамі. Перший спосіб подає граф у вигляді списку суміжних вершин. Для зображення графа G = (V ,E ) у вигляді списку суміжних вершин

використовують масив Adj із V списків по одному на вершину. Для

568

Розділ IV. АЛГОРИТМИ

кожної вершини u V список суміжних вершин Adj[u] містить у дові- льному порядку покажчики на всі суміжні з нею вершини (усі верши- ни v, для яких (u,v ) E ). Цей спосіб дає компактне зображення, особ-

ливо для розріджених графів. Його недолік: якщо треба дізнатися, чи є у графі ребро з u у v , то доводиться переглядати весь список. Ситуа- цію можна поліпшити, упорядкувавши множину пар лексикографічно.

Нехай граф G має n вершин. Другий спосіб зображує граф як матрицю суміжності A = (aij ) розміром n ×n . Усі вершини графа ну-

мерують числами від 1 до n . Тоді aij =1, якщо між i -ю та j -ю вер- шинами графа є ребро, і aij = 0 , якщо такого ребра немає. Цей спосіб

дозволяє швидко визначити, чи з'єднані дві вершини ребром, але по- требує більше пам'яті.

Зображення неорієнтованого графа обома способами:

1

2

3

4

Граф 1

Зображення графа 1 у вигляді списку суміжних вершин:

1 2 4 3

2 1 4

3 1 4

 

 

 

 

 

 

 

 

 

 

 

 

 

4

 

 

1

 

 

 

3

 

 

 

2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

і за допомогою матриці суміжності:

569

ПРОГРАМУВАННЯ

 

1

2

3

4

1

0

1

1

1

2

1

0

0

1

3

1

0

0

1

4

1

1

1

0

Неважко помітити, що для неорієнтованого графа сума довжин усіх списків суміжних вершин дорівнює подвоєній кількості ребер,

оскільки ребру (u,v ) відповідає по одному елементу зі списків Adj[u] та Adj[v]. До того ж матриця суміжності для неорієнтованого графа симетрична відносно головної діагоналі, тобто збігається зі своєю транспонованою матрицею, оскільки (u,v ) та (v,u) одне й те саме

ребро. Завдяки симетричності достатньо зберігати лише числа на го- ловній діагоналі й вище неї при цьому обсяг задіяної пам'яті змен- шується майже вдвічі.

Зображення орієнтованого графа обома способами:

1

2

3

4

Граф 2

Зображення графа 2 у вигляді списку суміжних вершин:

1 4 3

2 1 4

3 4

4 4

і за допомогою матриці суміжності:

570