Глава 7
Ітеративні та рекурсивні алгоритми
Ефективним засобом програмування для певного класу задач є рекурсія. З її допомогою можна вирішувати складні завдання чисельного аналізу, комбінаторики алгоритмів трансляції, операцій над списковыми структурами і т.д. Програми в цьому випадку мають невеликі обсяги порівняно з итерацией і вимагають менше часу на налагодження. Полрекурсией розуміють спосіб завдання функції через саму себе, наприклад спосіб завдання факторіалу у вигляді n! = (p - 1)! • n. У програмуванні під рекурсивним процедурою (функцією) розуміють спосіб звернення процедури (функції) до самої себе. Під итерацией розуміють результат багаторазово повторюваної будь-якої операції, наприклад подання факторіалу у вигляді n! = 1 • 2 • 3 • 4 •... • n. Серед широкого класу задач зручно представляти з використанням рекурсивних процедур (функцій) ті завдання, які зводяться на підзадачі того ж типу, але меншою розмірності. Загальна методика аналізу рекурсії містить три етапи: 1. Параметризація завдання, яка полягає у виділенні різних елементів, від яких залежить рішення, зокрема розмірності розв'язуваної задачі. Після кожного рекурсивного виклику розмірність повинна зменшуватися. 2. Пошук тривіального випадку та його рішення. Як правило, це ключовий етап у рекурсії, розмірність завдання при цьому часто дорівнює або 1. 3. Декомпозиція загального випадку, що має на меті привести завдання до однієї або декількох завдань того ж типу, але меншою розмірності. Розглянемо поняття ітеративного і рекурсивного алгоритмів на прикладі обчислення факторіалу.
7.1.
Ітеративний алгоритм
Н
айбільш
простий і природною формою
подання
ітеративного
алгоритму при реалізації на комп'ютері
є його опис з використанням циклу.
Розглянемо
алгоритм ітеративного алгоритму
обчислення факторіалу n!
Згідно
з визначенням функції n!
маємо
Спочатку в залежності від поточного значення відбувається вибір способу обчислення n!. Якщо n = O тo, змінна fctrl приймає значення 1. Якщо n≠ 0, в циклі обчислюється добуток 1 • 2 ... - п. Змінна r - це параметр циклу, який послідовно приймає значення 2, 3, 4 і т.д. до n включно. Для кожного значення параметра циклу виконується тіло циклу: fctrl:=fctrl* i. Послідовність ітерацій циклу для n = 6 показано в табл. 7.1. Під ітерацією циклу будемо розуміти виконання тіла циклу для конкретного значення параметра циклу. Програма складається з процедури-функції FACTORIAL і основної програми. В основній програмі відбувається введення значення N, виклик процедури-функції і друк результату.
7.2.
Рекурсивний алгоритм
Рекурентні відношення досить часто зустрічаються в математичних виразів. Рекурсія у визначенні полягає в тому, що визначається поняття визначається через саме це поняття. Рекурсія в обчисленнях виступає у формі рекурентних відношень, які показують, як обчислити чергове значення, використовуючи попередні.
Проведемо аналіз рекурсивного обчислення факторіалу. 1. Параметризація. В даному випадку є лише один параметр n - ціле число. 2. Пошук тривіального випадку. При n = 0 або n = 1 значення факторіалу дорівнює 1, що відповідає виходу з рекурсії. 3. Декомпозиція загального випадку: n! обчислюється через меншу розмірність цієї задачі (n - 1)!. Покажемо алгоритм обчислення факторіалу для n - 4 (мал. 7.1). Спочатку утворюється так званий рекурсивний кадр 1 n = 4. Кадр - структура, що містить деяку інформацію. Для цього кадру відводиться пам'ять і в ньому фіксуються всі значення змінних тіла функції при n = 4. Зазначимо, що у рекурсивному кадрі фіксуються всі значення змінних функції, крім глобальних. Потім відбувається виклик Factorial (n) при n = 3. Утворюється кадр 2, де фіксуються значення змінних тіла функції при n = 3.
При цьому кадр 1 також зберігається в пам'яті. З кадру 2 відбувається звернення до Factorial(n) при n = 2. В результаті цього звернення утворюється кадр 3, де фіксуються значення змінних тіла функції при n = 2 і т.д. до тих пір, поки при наступному зверненні до функції Factorial умова n > 0 не прийме значення False. Це відбудеться в кадрі 5. У цьому кадрі отримаємо значення Factorial = 1 і передамо це значення в кадр 4. Після цього кадр 5 буде знищений, так як звернення Factorial(n) при n = буде виконано. У кадрі 4 обчислимо значення Factorial(n) для n = 1. Після цього передамо це значення у кадр 3, а 4 кадр буде закрито, оскільки звернення до Factorial(n) при n = 1 буде закінчено. Так буде згортатися ланцюжок кадрів в послідовності, зворотного тієї, в якій їх породжували, поки не звернемо 1 кадр. Після цього обчислення функції буде закінчено. Програма рекурсивного алгоритму обчислення факторіалу буде мати вигляд:
Ф
ункція
обчислення факторіалу має наступні
особливості:
•
при обчисленні факторіалу відбувається
звернення функції до самої себе
(підкреслено у виразі), але з меншим
значенням аргументу п – 1 в порівнянні
з першим викликом
n:
• при обчисленні факторіалу не використовується цикл, що є істотною особливістю рекурсивного алгоритму. Розглянемо послідовність дій під час рекурсивного обчисленні факторіалу для n = 3: 1) зовнішній виклик з основної програми Factorial(3) 2) перший рекурсивний виклик Factorial (2) в операторі
н
іякі
обчислення
—
тільки
виклик
(підкреслено);
другий рекурсивний виклик Factorial(1); 2) отримання значення Factorial (1) := 1; 3) повернення з другого рекурсивного виклику і обчислення факторіалу Factorial(2) := 1 * 2 = 2; 4) повернення з першого рекурсивного виклику і обчислення факторіалу Factorial(3) := 2 * 3 = 6; 5) повернення в основну програму Fac := 6.
З
амечание.
В программу рекурсивного вычисления
факториала можно добавить стандартную
функцию текущего времени
7.3.
Рекурсивні структури даних
Список-набір елементів, розташованих у певному порядку. Список черговості - список, в якому останній вступник елемент додається до нижній частині списку. Список з використанням покажчиків - список, в якому кожен елемент містить вказівник на наступний елемент списку. Односпрямований і двонаправлений список - це лінійний список, в якому всі винятки та додавання відбуваються в будь-якому місці списку. Односпрямований список відрізняється від двонаправленого списку тільки зв'язком, тобто в однобічному списку можна пересуватися тільки в одному напрямку (з початку кінець), а в двонаправленого - у будь-якому (мал. 7.2).
Чергу
- тип даних, при якому нові дані
розташовуються слідом за існуючими в
порядку надходження; надійшли першими
дані при цьому обробляються першими.
Чергу
іноді називають циклічної пам'яттю або
списком типу FOFO («first-in-first-out» - «першим
прийшов - першим обслуговується»). Іншими
словами, у черзі є голова і хвіст.
У
черзі новий елемент додається тільки
з одного кінця (мал. 7.5).
Видалення елемента відбувається на іншому кінці. В даному випадку це може бути тільки четвертий елемент. Чергу - по суті односпрямований список, тільки додавання і вилучення елементів відбувається на кінцях списку. Стек - лінійний список, в якому всі включення і виключення робляться в одному кінці списку. Стек називають (pust-down) списком, реверсивній пам'яттю, гнiздовiй пам'яттю, магазином, списком типу LIFO (“last-input-first-out” - «останнім прийшов - першим обслуговується»). Стек - частина пам'яті ОЗУ комп'ютера, яка призначається для тимчасового зберігання байтів, використовуваних мікропроцесором. Дії зі стеком виробляються за допомогою регістра покажчика стека. Будь-яке пошкодження цій частині пам'яті призводить до фатальних краху. Логічна структура стека представлена на рис. 7.6.
Рис.
7.6. Структура стека
Найважливіші операції доступу до стеку - включення елементів і виключення елементів - здійснюються з вершини стека, причому в кожен момент для виключення або включення доступний один елемент, що знаходиться на вершині стека. Вершина стека адресується за допомогою спеціального покажчика. Для включення нового елемента в стек покажчик спочатку переміщається «вгору» (можлива й інша конфігурація стека, коли стек «надбудовується знизу») на довжину слота, або комірки, а потім за значенням індексу (індексу) в стек міститься інформація про новий елемент. При виключенні елемента зі стека спочатку прочитується інформація про виключені елементи за значенням індексу (індексу), а потім покажчик зміщується «вниз» (або «вгору» при зворотному конфігурації стека) на один слот. Стек вважається порожнім, якщо вказівник зміщений «вниз» на довжину одного осередку щодо нижньої межі стека. Стек вважається повним, якщо вершина стека (покажчик стека) поєднується з верхньою межею стека. Приклад 1. Нехай є множина елементів {1, 4, 6, 9} і до стеку застосована послідовність операцій: (I,I,I,О,I,О), де символ I означає запис елемента в стек, а символ О зчитування елемента зі стека. Етапи виконання операцій показані на мал. 7.7.
Дек (стек з двома кінцями) - лінійний список, в якому всі включення і виключення робляться на обох кінцях списку (мал. 7.8). ыце один термін «архів» застосовувався до декам з обмеженим виходом, а деки з обмеженим входом називали «переліками», або «реєстрами».
Циклічно пов'язаний список має ту особливість, що зв'язок його останнього вузла іде назад до першого вузла списку (мал. 7.9).
У цьому випадку можна отримати доступ до будь-якого елемента, що знаходиться в списку, відправляючись від будь-якої заданої точки. При цьому не доводиться розрізняти у списку «останній» або «перший» вузол.
7.4.
Види обходу бінарних дерев
Набір способи обходу дерева дозволяє ввести ставлення порядку для вузлів дерева. Найбільш поширені три способи обходу вузлів дерева (мал. 7.10), які отримали наступні назви: • обхід зліва направо (зворотній порядок, инфиксная запис); • зверху вниз (прямий порядок, префиксная запис);
Можливо опис цього арифметичного виразу з допомогою бінарного дерева - мал. 7.12. Для проходження цього дерева в прямому порядку починаємо з кореня (вузол +). Потім пройдемо в прямому порядку ліве піддерево (з коренем, відміченим *) і далі праве піддерево у прямому порядку
Рис. 7.11. Способы обхода бинарного дерева
Рис.
7.12. Бинарное дерево для описания
арифметического выражения
(з коренем, відміченим /). Проходження лівого піддерева у прямому порядку починається з кореня (позначені *), потім слід пряме проходження його лівого піддерева (породжує послідовність-АВ) і далі правого піддерева (послідовність С). Тому вся послідовність, що породжуються проходженням лівого піддерева основного кореня, буде *-АВС. Аналогічно проходження у прямому порядку правого під дерева з корінням, відміченим /породжує послідовність /D * *ЕF. Таким чином, проходження всього дерева в прямому порядку породжує послідовність
Походження цього дерева в зворотному і кінцевому порядках породжує відповідно послідовності:
Зауважимо, що у всіх трьох виразах порядок входження змінних збігається; змінюється тільки порядок знаків операцій. При цьому жодне з цих виразів не має дужок, і, таким чином, якщо не вказані правила пріоритету, значення наведеного вище вираження в инфиксной формі не можна обчислити однозначно. Для бінарного дерева на рис. 7.13 маємо наступний порядок обходу вузлів:
Контрольні питання 1. Що розуміється під рекурсією і итерацией в математики? 2. Які особливості ітеративного алгоритму? 3. Які особливості рекурсивного алгоритму? 4. У чому полягає методика аналізу рекурсивного алгоритму? 5. В яких випадках доцільно використовувати рекурсивний або ітеративний алгоритм? 6. Наведіть приклади ітерації і рекурсії. 7. Чи всі мови програмування дають можливість рекурсивного виклику процедур? 8. Вкажіть види обходу бінарних дерев. 9. Наведіть приклад рекурсивним структури даних. 10. Що таке покажчики і динамічні змінні в алгоритмічні мови?
