Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ФУНКЦІЇ ЗДАВАТИ.doc
Скачиваний:
1
Добавлен:
01.05.2025
Размер:
611.33 Кб
Скачать

Void Menu (void);

ACTION * GetAction (void);

Int main (void)

ACTION 'pact, /' вказівники на функції вибраного і */

* pdone - NULL; /* попередньо виконаного пункту меню */

Мепи(); /* висвітлення меню */

do 1 /* цикл виконання пунктів меню */

pact - GetAction{); /* вибір Функції пункту меню '/

('pact)(pdone); /* виконання вибраного пункту "/

pdone-pact; /* фіксація виконаного пункту меню "/

) while (pdone !-Exitltem); /• поки не виконано пункт виходу •/

return 0;

1

Void Menu (void) /* функція формування меню */

clrscr(); /* очицення екрана "/

puts ("\t Набір дій:"); puts ("\t 1 - Варіант дій 1") puts ("\t 2 - Варіант дій 2") puts ("\t 3 - Варіант дій З") puts ("\t 4 - Завершення роботи");

)

ACTION* GetAction (void) /* функція вибору функції пункту меню */

І

ACTION * fun_arr[] - litem!. Item2, Item3, Exitltem); /* масив

вказівників на функції пунктів меню */

Int nuro, nitems;

піterns - sizeof(fun_arr) /sizeof(ACTION *); /* кількість пунктів */

do ( /* введення номера пункту меню */

printf ("ХпВибір -> ");

num - getche( ) - '0 '; /* перетворення символа в число */

printf(n\n");

) while (num < 1 II num>nitems); /* повторення у разі помилки */

return fun arr[num-l); /* повернення вказівника на вибрану функцію */

1

ACTION Iteml /' функція пункту 1 меню */

(

If (pred — null) (

puts(" ** Початок дій **");

/* . - - - дії п.1, коли розпочинається робота програми */

і

/" . . . - інші дії п.1. */

puts (" Виконано завдання п.1.");

І

ACTION Item2 /* функція пункту 2 меню */

І

/* ... - дії п.2. */

puts (" Виконано завдання п.2.и);

action Item3 /* функція пункту 3 иеню */

{

/* ... - дії п.З */

if ( (ACTION* )pred == Item2) { /* якщо попереднім був пункт 2 */

puts(" Виконано завдання п.З (після п.2).");

return; ) ■»

/* ... - інші дії п.З */ puts (" Виконано повне завдання п.З.");

ї

action Exitltem /* функція пункту 4 меню */

{

/* . . . - завершальні дії програми */ puts(" ** Роботу програми завершено **");

»

Приклад виконання:

Набір дій:

  1. - Варіант дій 1

  2. - Варіант дій 2

  3. - Варіант дій З

  4. - Завершення роботи

Вибір -> 1

** Початок дій **

Виконано завдання п.1. Вибір -> З

Виконано повне завдання п.З. Вибір -> 2

Вихонано завдання п.2. Вибір -> З

Виконано завдання п.З (після п.2).

Вибір -> 4

** Роботу програми завершено **

Розділи меню реалізовано в програмі як набір однотипних функцій; Iteml (ї, Item2(), Item3(), ExitltemO, які не повертають значення та мають один формальний параметр-вказівник pred з базовим типом void. Тип цих функцій задекларовано як тип ACTION. Параметр функцій використовується в main() для передавання адреси тієї функції, що виконувалась перед цим, у функцію, яка викликається для реалізації вибраного пункту меню. Функція Iteml () аналізує отримане значення, щоб переві­рити, чи це перше звертання до меню, а функція Item3 () реалізує різні дії залежно від того, який пункт меню їй передував (зверніть увагу на операцію перетворення типу (action * )pred, що необхідна для порівняння функцій).

Функція main () керує циклічним процесом роботи з меню. Першою викликається функція GetAction(), яка повертає адресу тієї функції, що повинна реалізовувати вибраний користувачем пункт меню. Ця адреса записується у вказівник pact.

Наступний оператор ( * pact)(pdone);

викликає функцію за адресою, яка записана в pact, і передаєш через параметр pdone адресу функції, що виконувалась перед цим. Коли робота функції {*pact) ( ) завер­шується, pdone отримує її адресу, і процес звертання до меню повторюється знову. Робота програми припиняється, коли виконано завершальну функцію Exitltemf >.

Вибір функції, що відповідає вибраному користувачем пункту меню, виконує функція GetAction {). Функція працює з масивом вказівників на функції fun_arr, елементи якого проініціалізовані адресами відповідних функцій пунктів меню:

ACTION * fun_arr[] - ( Iteml, Item2, Item3, Exitltem );

Кількість елементів масиву визначається кількістю імен функцій у списку ініціалізації:

nitems = sizeof(fun_arr)/sizeof(ACTION*);

У разі виклику GetAction ( ) відбувається зчитування клавіші з номером вибра­ного пункту меню і формується число num - getche {) - ' 0 '. Якщо num потрапляє в діапазон 1..nitems, то в функцію maint ) передається адреса функції вибраного пункгу меню* інакше процес вибору ловторюється. Адреса функції вибраного пункту меню задається як елемент масиву вказівників fun_arr з індексом num-1.

11.10. Рекурсивні функції

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

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

Рекурсивне та ітераційнс обчислення ряду Фібоначчі. Першою розглянемо функ­цію, призначену для знаходження /?-:о числа Фібоначчі. За математичним означенням два перших числа Фібоначчі (з номерами 1 та 2) дорівнюють 1, а кожне наступне об­числюється як сума двох попередніх. Означення ряду Фібоначчі є рекурентним (щоб знайти /ї-е число, треба просумувати (п-І)-е та (л-2)-е числа Фібоначчі), тому для його програмної реалізації спробуємо застосувати відповідну рекурсивну функцію.

/* Визначення числа Фібоначчі за заданим номером */ long FibonNumb (int n)

ї

if {n—1 II n — 2) return 1; /* перше та друге число */

return FibonNumb(n-l) ♦ FibonNumb(n-2); /• всі наступні числа */

ї

Наведена функція програмно відтворює означення числа Фібоначчі. Вона дуже коротка (перевірку допустимості значення параметра п опущено навмисно), тому наочно демонструє, як організовані рекурсивні звертання в функціях.

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

Щоб детальніше пояснити, як виконуються рекурсивні внкликн. звернемось до функції FibonNumb () для знаходження, наприклад, 12-ю за порядковим номером числа Фібоначчі. Оскільки 12 >2, то з двох операторів тіла функції FibonNumb О ви­бирається другий, який фапично буде таким:

return FibonNumbt11) + FibonNumb*10);

Рис. 11.2. Схема рекурсивних викликів FibonNumbO для обчислення 12-го числа Фібоначчі

Щоб реалізувати цей оператор, необхідно двічі викликати функцію FibonNumbt ї для менших за значенням аргументів. У свою чергу, обчислення FibonNumb (11) по­гребує обчислення 10-го і 9-го чисел Фібоначчі, а для знаходження 10-го числа треба викликати FibonNumbt 9) та FibonNumb(8) і т. д. (рис. 11.2). Цей процес називають рекурсивним зануренням. Виклик FibonNumbO) зумовить виконання оператора

return FibonNumb(2) + FibonNumb{1);

Обидва звертання цього оператора повернуть значення 1, оскільки це значення двох перших чисел Фібоначчі, які становлять базис функції. Процес занурення по даній віші припиняється і розпочинається зворотний процес рекурсивного повернення. Сума FibonNumbt 1) та FibonNumbt 2) є значенням, яке повергає функція FibonNumbO).

11.10. Р«КурСИВНІ ФУНКЦІЇ ■■■■■ 223

Обчислене значення передається у функцію, яка реалізує виклик FibonNumb(4). Але, щоб знайти четверте число Фібоначчі, ця функція повинна також реалізувати повторне звертання до FibonNumb(2). Значення FibonNumbH) передається у функцію, яка обчислює п'яте число Фібоначчі і т. д. Процес рекурсивного повернення завершиться, коли повністю виконається найперший виклик функції, тобто FibonNumb (12).

□Обов'язковим елементом кожної рекурсивної функції повинна бути умова за­вершення роботи. В функції FibonNumb() вона задається оператором

if (n«l II Ii—2) return 1;

який становить базис даної рскурсії. У разі відсутності умови завершення або її помилковості рекурсивні виклики призведуть до "зависання" програми, оскільки рекурсивна функція буде викликати сама себе безконечно.

Реалізація наведеної функції, яка вимагає великої кількості повторних викликів FibonNumb() для тих самих значень аргументів, вочевидь неефективна (пропонуємо самим підрахувати, скільки раз буде викликатись, наприклад, FibonNumb(4) у про­цесі обчислення 12-го числа Фібоначчі). Для порівняння запишемо ітераційний варіант функції обчислення заданого числа Фібоначчі.

/• Визначення числа Фібоначчі - ітераційний варіант */

long FibonNumb_iter (int n) 1

int k;

long fibpl, fibp2, fib; if (n < 3) return 1;

fibpl = fibp2 "1; /* два визначені числа */

2; /* ноиер останнього визначеного числа */

do (

fib - fibpl + fibp2; /' наступне число */

fibp2-fibpl; /* зміна значень попередніх чисел */

fibpl - fib;

) while (++k !-n); return fib;

За простотою та наочністю ця функція поступається рекурсивній. Проте в про­цесі роботи вона одноразово обчислює значення кожного числа Фібоначчі, внаслідок чою її швидкодія значно вища, ніж у попередньої рекурсивної функції. Щоб відчути різницю в часі виконання цих двох функцій, спробуйте знайти 45-тс число Фібоначчі (передостаннє з чисел, що потрапляють у діапазон даних з типом long int).

Рекурсія в алгоритмі Евкліда. Наведемо ше один приклад рекурсивної функції. Вона призначена для знаходження найбільшого спільного дільника (НСД) двох цілих чисел за відомим рекурсивним метолом Евкліда. Цей метод базується на тому, що НСД двох цілих чисел збігається з НСД меншого з цих чисел і залишку від ділення більшого числа на менше.

/* b - НСД •/

/* переставлення чисел місцями */ /* рекурсивне зменшення чисел */

Дана функція не тільки лаконічна в записі та проста для читання і розуміння. Вона також ефективна й достатньо швидка, оскільки рекурсивні виклики здійснюються лінійно, а глибина їх вкладень (це властивість алгоритму Евкліда) не буває великою. Наприклад, обчислення NSD(405, 1026) зумовить виклик NSD(1026, 405), а далі послідовно викликатимуться NSD(405, 216), NSD<216, 189) і NSDU89, 27). Останній виклик функції поверне значення 27, яке буде передане у функцію, що здійснила цей виклик. У свою чергу, ця функція відразу поверне отримане значення в попередню функцію, оскільки виконується оператор

return NSD(b, а % b) ;

Зворотний процес завершиться після повернення в функцію, яка зробила найперший виклик: NSD(405, 1026).

Хвостова та зворотна рекурсія. Рекурсивними викликами можна замінити циклічні процеси. Подамо приклад короткої функції, призначеної для виведення на екран k по­слідовних елементів заданого масиву дійсних чисел. Функція використовує рекурсивне звертання замість традиційного для задач такого роду оператора циклу.

/* Виведення масиву дійсних чисел */ void PrintArray (double arr(], int k) і

if (k « 0) return; /* елементів немає */

printf ("%8.21f", *arr); /* виведення першого елемента »/

PrintArray(arr+1, k-1); /* продовження для решти масиву •/

У всіх трьох розглянутих вище функціях рекурсивні звертання були останніми опе­раторами функцій. Такий варіант рекурсії називають хвостовим. Проте інколи доцільно застосувати іншу організацію рекурсивної функції. Розглянемо наступну функцію, яка подібна до PrintArray (ї, але внаслідок зміни порядку операторів роздруковує масив у зворотній послідовності - від останнього елемента до першого.

/* Виведення масиву дійсних чисел у зворотному порядху */ void ReversPrint (double arr[], int k)

if <k> 1)

ReversPrint(arr+l, k-l) ; printf <"%8.21f'\ *arr);

/• просування no масиву */ /* виведення елементів */

11.10. Рекурсивні функції

225

Виведення елементів масиву розпочнеться тільки тоді, коли к дорівнюватиме 1, отже агг вказуватиме на останній елемент масиву. Буде надруковано цей елемент і керування перейде у викликаючу функцію. У цій функції ще залишився не виконаним останній оператор, шо друкує * агг, тому на екран виводиться значення передостаннього елемента масиву і продовжується зворотний процес рекурсивного повернення.

Два останні приклади демонструють важливі властивості рекурсивних функцій:

І) оператори, записані перед рекурсивним викликом, виконуються в тому ж по­рядку, в якому відбуваються виклики функції;

2) оператори, розташовані після рекурсивною виклику, виконуються в зворотному порядку відносно рекурсивних викликів даної функції.

Кожен виклик рекурсивної функції пов'язаний зі створенням окремого набору параметрів і внутрішніх змінних — саме їх значення використовуються у процесі реа­лізації даного виклику. Для збереження даних, що створюються у процесі звертання до функції, використовується спеціальна область оперативної пам'яті, яка називається стеком. Особливість організації стека в тому, шо записані в нього дані зчитуються в послідовності, зворотній до послідовності їх запису (таку форму запису/читання даних ще називають LIFO - last in, first oui - останнім прийшов, першим вийшов). З кож­ним звертанням до функцій програми стек збільшується внаслідок запису параметрів і виу грішніх змінних цієї функції. Дані активізованої функції зберігаються у стеку до моменту завершення її роботи, після чого пам'ять, зайнята даними функції, звільняється. Найпершою може завершити свою роботу тільки функція, що була викликана остан­ньою. Тому послідовність звільнення стека є зворотною до порядку виклику функцій у про* рамі. Якщо реалізація певної задачі пов'язана зі значною кількістю рекурсивних звертань до функції (прикладом може слуїувати функція FibonNumb ( ) у разі великих тачень параметра п>. то цілком імовірною стає загроза переповнення стека. Фізичний обсяг стека залежить від апаратно-проірамних особливостей комп'ютера і встановленої моделі пам'яті.

Рекурсивний пошук та інші задачі. Останній приклад- рекурсивна функція для пошуку заданого елемента у масиві цілих чисел, впорядкованих за зростанням значень. Функція повертає вказівник на знайдений елемент (у разі потреби індекс елемента можна визначити через різницю адрес знайденого елемента і початку масиву) або NULL, якщо такого елемента немає. З урахуванням впорядкованості елементів масиву, застосуємо для пошуку метод половинного ділення. Перевіривши серединний елемент, визначаємо, в якій половині масиву: молодшій чи старшій - має бути розташоване шукане значення. Далі аналогічним чином перевіряємо тільки цю частину масиву. Пошук завершується, коли елемент знайдено, або коли перевірено останній можливий елемент масиву.

Порівняно з лінійним пошуком, який вимагає послідовного перегляду елементів масиву (можливо, що всіх), пошук за методом половинного ділення дає змогу значно зменшити кількість операцій перевірки. У найгіршому випадку лінійний пошук вимага­тиме N операцій порівняння елементів, а бінарний-тільки [logJV]+l, де N - кількість елементів масиву, а квадратні дужки [] позначають цілу частину числа.

/' Пошук заданого елемента nfind у впорядкованому масиві */