
Переваги і недоліки рекурсії
Рекурсія являє собою могутній засіб. У теорії доводиться, що клас рекурсивних алгоритмів ширше класу ітеративних алгоритмів, де заборонені рекурсивні виклики. Рекурсивні алгоритми найбільш придатні в тих випадках, коли поставлена задача або використовувані дані визначені рекурсивно. Але це не означає, що навіть у цьому випадку рішення буде найкращим. Швидше за все такий алгоритм буде коротким, виразним, але не найефективнішим. Рекурсія накладає тверді вимоги до використання апаратного стека персонального комп'ютера. Саме при наявності рекурсивних процедур необхідно подбати про перерозподіл пам'яті між стеком і купою. У системі ТУРБО ПАСКАЛЬ це можна зробити за допомогою директиви компілятора $M:
{$M Розмір_стека, Мінімум_купи, Максимум_купи}
Вона повинна використовуватися в перших рядках основної програми. Діапазон значень для розміру стека від 1024 до 65535 байт (1K .. 64K). Стандартно під стек виділяється 1K. Якщо глибина рекурсії велика, то це значення необхідно збільшити.
Витрати на один рекурсивний виклик визначаються одним викликом процедури. Тому вони не так вуж і великі. Але якщо програміст погано спроектував алгоритм, те ці витрати за рахунок великого числа рекурсивних викликів можуть виявитися істотними. Часто саме невідповідні приклади використання рекурсії відштовхують від неї.
Наприклад. числа Фибоначчи.
fibi+2 = fibi+2 + fibi+2 , при n>1
fib1 = 1
fib0 = 0
Якщо вирішувати цю задачу напряму, то одержимо:
function fib(n: byte): word;
begin
if n=0 then fib:=0
else
if n=1 then fib:=1
else
fib:=fib(n-1) + fib(n-2)
end;
При обчисленні fibn звертання до функції fib(n) приводить до экспоненциально зростаючої кількості активацій цієї процедури. Причому обчислення тих самих значень дублюються. Ясно, що така реалізація непридатна для практичного використання. При обчисленні fib5 буде потрібно 15 викликів функції fib. Ці 15 викликів можна зобразити у виді такого дерева.
Мал. 6. Дерево викликів рекурсивної процедури fib для виклику fib(5).
Чому так відбувається? Справа в тім, що програма обчислення чисел Фибоначчи і програма рішення задачі про Ханойську вежу засновані на СПАДНИХ РЕКУРСИВНИХ ОБЧИСЛЕННЯХ: відштовхуючись від більш загального випадку, ми здійснюємо послідовний рекурсивний спуск до тривіального рішення. Якщо для задачі про Ханойську вежу це рішення, у принципі, задовільне (можна довести, що всі перекладання кілець необхідні), то повторне обчислення вже раніше обчисленого числа Фибоначчи ніяк не можна визнати задовільним. Тут нам може допомогти прийом побудови рекурсивних алгоритмів, протилежний спадному рекурсивному обчисленню: відправляючись від тривіальних ситуацій, “піднятися” до випадку, що вимагає рішення. Такі обчислення прийняте називати ВОСХІДНОЮ РЕКУРСІЄЮ.
Восхідні рекурсивні обчислення не завжди можливі. Але коли цей спосіб можливо застосувати, він дозволяє одержати більш ефективні програми. З визначення числа Фибоначчи видно, що кожне наступне число виходить як сума двох попередніх, тобто обчислення fibk вимагає знання fibk-2 і fibk-1. Відправляючись від fib0 і fib1 будемо їх обчислювати итеративно по черзі, поки не дійдемо до потрібного нам fibn. Уведемо три цілі змінні, f і f1 і будемо вимагати
0 i n
(**) f = fibi
f1 = fibi+1
Це умова повинна виконуватися перед виконанням ітерацій і після кожної ітерації, тобто умова (**) є інваріантом циклу. У випадку, коли i стане рівним n з умови (**) випливає, що f = fibn, тобто ми одержимо рішення нашої задачі.
Тв. ( n 0)
i := 0; f := 0; f1 := 1;
Тв. (**)
while i<>n do begin
Тв. (**)
i := i+1; { Стратегія руху до цілі }
Тв. ( 0<in; f = fibi-1; f1 = fibi )
t := f; f := f1;
Тв. ( 0<in; t = fibi-1; f = f1 = fibi )
f1 := f + t;
Тв. ( 0<in; f = fibi; f1 = fibi+1, тобто умова (**) )
end;
Тв. ( i = n; f = fibi f = fibn, що і було потрібно одержати.)
У такий спосіб ми одержали ітеративне рішення задачі обчислення чисел Фибоначчи.
Тепер розглянемо процес пошуку висхідних рекурсивних обчислень у загальному виді. Нехай функція f визначається формулою виду
f(x) = u(f, x),
де u(f, x) є вираз, що включає функцію f, застосовувану до аргументів, що залежать від x. Узагалі говорячи, u(f, x) включає умовний вираз типу
fact(x) = ЯКЩО x=0 ТЕ 1 ІНАКШЕ x*fact(x-1) ВСЕ-ЯКЩО
Розглянемо графік G функції U, тобто множину пар [x, f(x)] (розуміючи під x у загальному випадку вектор x1, x2, …, xn, якщо f залежить від n аргументів). Для всякого x, такого, що функція f(x) визначена, тобто такого, що
= [x, f(x)] G
маємо
[x, f(x)] = [x, u(f, x)].
Це рівняння можна розглядати як рівняння на графіку G виду
= F(,, …),
де ,,, … - елементи графіка, а F – “створююча” функція, що дозволяє виявити нові елементи, виходячи з відомих елементів графіка.
Саме ця функція F буде використовувати висхідний спосіб: відправляючись від “тривіальних” елементів графіка, тобто пари [x,f(x)], таких, що f(x) обчислювана без рекурсивного виклику, G поповнюється за допомогою послідовності ітерацій до одержання пари (чи набору), де перший елемент є аргумент x0 для якого визначається значення f0.
Для факторіала маємо
x=0, y=1,
тобто крапку графіка [0,1]. З рекуррентного визначення маємо
[x,y] = [a+1, (a+1)*b],
де [a,b] є елемент графіка. Іншими словами, функція F перетворюэ графік G у графік G рівний G, збільшеному на усіх пари [a+1, (a+1)*b], де [a,b]G0. Це підказує восхідний спосіб обчислення factn виходячи з графіка, що містить тільки тривіальний елемент [0,1] шляхом послідовного поповнення на кожнім етапі на [a+1, (a+1)*b], де [a,b] – останній отриманий елемент. Цей ітеративний метод, належним чином формалізований подібний з рішенням математичних рівнянь “методом нерухомої крапки”.
Висхідні рекурсивні обчислення допомагають шукати ітеративні рішення. Рекурсії варто уникати, коли э очевидне ітеративне рішення. Розглянемо загальний випадок. Нехай у нас э рекурсивна схема:
P if B then begin S; P end;
чи еквівалентна їй
P (S; if B P)
Потрібно прагнути привести її до виду
P x := x0; while B do S;