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

9. Рекурсія

9.1. Рекурсивно-визначені процедури і функції

Описання процедури А, в розділі операторів якої використовується оператор цієї процедури, називається рекурсивним. Таким чином, рекурсивне описання має вид

Procedure A(u, v : ParType);

...

Begin

...; A(x, y); ...

End;

Аналогічно, описання функції F, в розділі операторів якої використовується виклик функції F, називається рекурсивним. Рекурсивне описання функції має вид

Function F(u, v : ArgType) : FunType;

...

Begin

...; z := g(F(x, y)); ...

End;

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

Приклад 9.1. Факторіал.

Розглянемо рекурсивне визначення функції n!=1*2*...*n (n-факторіал). Нехай F(n) = n! Тоді

1.F(0) = 1

2.F(n) = n*F(n - 1) при n > 0

Засобами мови це визначення можна сформулювати як обчислення:

If n = 0

then F := 1

else F := F(n - 1) * n

Оформивши це обчислення як функцію і змінивши ім’я, отримаємо:

Function Fact(n: Integer): Integer;

Begin

If n = 0

then Fact := 1

else Fact := Fact(n - 1) * n

End;

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

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

Tfact(n) = Tfact(n-1) + Tm + Tl + Tс

Для того, щоб обчислити Tfact(n), треба здійснити одну перевірку, одне множення і один виклик Tfact(n-1). Виклик Tfact потребує затрат часу Tc на “адміністративні” обчислення: передачу параметра, запам’ятовування адреси повернень і т.п. Поклавши С = Tm+Tl+Tс, отримаємо Tfact(n) = Tfact(n-1) + С. Неважко тепер показати, що Tfact(n) = Сn.

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

Спробуємо звести задачу до декількох підзадач, більш простих, ніж вихідна.

Нехай S = [ s1, s2, ..., sn ] - набір символів.

Через Permut(S) позначимо множину всіх перестановок S, a через Permut(S,i) - множину всіх перестановок, в яких на останньому місці стоїть елемент si. Тоді

Permut(S) = Permut(S,n)  Permut(S, n-1)  ...  Permut(S,1)

Елемент множини Permut(S, i) має вид [sj2,..., sjn,si] де j2,..., jn - всі можливі перестановки індексів, не рівних I. Тому Permut(S, i) = (Permut(S \ si), si) і Permut(S)=(Permut(S \ s1),s1)+...+ (Permut(S \ sn), sn).

Отримане співвідношення виражає множину Permut(S) через множини перестановок наборів з (n-1) символу. Доповнивши це співвідношення визначенням Permut(S) на одноелементній множині, отримаємо:

1. Permut({s}) = {s}

2. Permut(S) = (Permut(S\s1), s1) + ... + (Permut(S\sn), sn)

Уточнимо алгоритм, опираючись на представлення набору S в виді масиву S[1..n] of char.

По перше, визначимо параметри процедури Permut:

k - кількість елементів в наборі символів;

S - набір символів, що переставляються.

Алгоритм починає роботу на вхідному наборі і генерує всі його перестановки, що залишають на місці елемент s[k]. Якщо множина перестановок, в яких на останньому місці стоїть s[j], уже породжена, міняємо місцями s[j-1] і s[k], виводимо на друк отриманий набір і застосовуємо алгоритм до цього набору. Параметр k керує рекурсивними обчисленнями: ланцюжок викликів процедури Permut обривається при k = 1.

Procedure Permut(k : Integer; S : Sequence);

Var

j : integer;

Begin

if k <> 1

then Permut(k - 1, S);

For j := k - 1 downto 1 do begin

Buf := S[j];

S[j] := S[k];

S[k] := Buf;

WriteSequence(S);

Permut(k - 1, S)

end

End;

Begin { Розділ операторів програми}

{Генерація вихідного набору S}

WriteSequence(S); {Виведення першого набору на друк}

Permut(n, S)

End.

Оцінимо складність алгоритму за часом у термінах C(n): Кожний виклик процедури Permut(k) містить k викликів процедури Permut(k-1) і 3(k-1) пересилання. Кожний виклик Permut(k-1) супроводжується передачею масиву S як параметра-значення, що за часом еквівалентне n пересиланням. Тому мають місце співвідношення

C(k) = kC(k-1) + nk + 3(k-1), C(1) = 0, звідки С(n) = (n+3)n!

Оцінимо тепер розмір пам’яті, необхідної для алгоритму. Оскільки S - параметр-значення, при кожному виклику Permut резервується n комірок (байтів) для S, а при виході з цієї процедури пам’ять звільняється. Рекурсивне застосування Permut призводить до того, що ланцюжок вкладених викликів має максимальну довжину (n - 1):

Permut(n) --> Permut(n-1) --> ... --> Permut(2) --> Permut(1)

Тому дані цього ланцюжка потребують n2 - n комірок пам’яті, тобто алгоритм потребує пам’ять розміром O(n2). Кількість перестановок – елементів множини Рermut(S) дорівнює n!. Тому наш алгоритм “витрачає” C(n)/n! = O(n) дій для породження кожної перестановки. Зрозуміло, такий алгоритм неможна називати ефективним. (Інтуїція нам підказує, що ефективний алгоритм повинен генерувати кожну перестановку за фіксовану, незалежну від n кількість дій.) Джерело неефективності очевидне: використання S як параметра-значення. Це дозволило нам зберегти незмінним масив S при поверненні з рекурсивного виклику для того, щоб правильно переставити елементи, готуючи наступний виклик.

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

Program All_permutations;

Const

n = 4;

Type

Sequence = array[1..n] of char;

Var

S : Sequence;

Buf : char;

i : Integer;

Procedure WriteSequence;

begin

For i := 1 to n do Write(S[i]);

Writeln

end;

Procedure Permut(k : Integer);

Var

j : integer;

Procedure Swap(i, j : Integer);

begin

Buf := S[j];

S[j] := S[i];

S[i] := Buf

end;

Begin

if k > 1

then Permut(k - 1);

For j := k - 1 downto 1 do begin

Swap(j, k); {Пряма перестановка}

WriteSequence;

Permut(k - 1);

Swap(k, j) {Обернена перестановка}

end

End;

Begin

{Генерація вихідного набору S}

WriteSequence; Permut(n)

End.

Тепер оцінка C(n) виходить з співвідношень

C(k) = kC(k-1) + 3(k-1), C(1) = 0 , т.е. C(n) = O(n!)

Цікаво, що цей варіант не потребує і пам’яті розміру O(n2) для збереження масивів. Необхідна тільки пам’ять розміру O(n) для збереження значення параметра k.