Rekurencja

Transkrypt

Rekurencja
Rekurencja
Rekurencja, zwana także rekursją (ang. recursion, z łac. recurrere, przybiec z powrotem) to
w logice, programowaniu i w matematyce odwoływanie się np. funkcji lub definicji do samej
siebie.
W logice wnioskowanie rekurencyjne opiera się na założeniu istnienia pewnego stanu
początkowego oraz zdania (lub zdań) stanowiącego podstawę wnioskowania (przy czym, aby
cały dowód był poprawny, zarówno reguła, jak i stan początkowy muszą być prawdziwe). Istotą
rekurencji jest tożsamość dziedziny i przeciwdziedziny reguły wnioskowania, wskutek czego
wynik wnioskowania może podlegać tej samej regule zastosowanej ponownie.
Na prostym przykładzie:
reguła: każdy ojciec jest starszy od swojego syna; każdy ojciec jest czyimś synem
stan początkowy: jestem 22-letnim mężczyzną
teza: ojciec ojca mojego ojca jest starszy ode mnie
dowód:
1. mój ojciec jest starszy ode mnie
2. mój ojciec jest czyimś synem
3. ojciec mojego ojca jest starszy od mojego ojca
4. ojciec mojego ojca jest czyimś synem
5. itd.
Na przykładzie zastosowań matematycznych poniższa definicja ciągu Fibonacciego jest
rekurencyjna:
, dla
gdyż definiuje funkcję odwołując się w definicji do niej samej.
Każda definicja rekurencyjna potrzebuje przynajmniej jednego przypadku bazowego (nie
rekurencyjnego), w tym przypadku są to wartości dla 0 i1. W przeciwnym wypadku nigdy się nie
zakończy.
Dla przykładu, obliczenie
wygląda następująco:
Innym przykładem jest wyliczanie największego wspólnego dzielnika za pomocą algorytmu
Euklidesa
Rekurencja jest podstawową techniką wykorzystywaną w funkcyjnych językach programowania.
Należy jednak zachować ostrożność przy używaniu rekurencji w rzeczywistych programach.
Ryzyko istnieje szczególnie przy przetwarzaniu dużej ilości głęboko zagnieżdżonych danych.
Jeśli program nie jest w rzeczywistości rekurencyjny, to rekurencja może dramatycznie
zwiększyć złożoność obliczeniową. Ponadto rekurencja zawsze zwiększa pamięciowe
zapotrzebowanie programu (chyba że zostanie użyta możliwa w pewnych przypadkach
optymalizacja zwana rekursją ogonową), gdyż wymaga ona zapamiętania m.in. adresów powrotu,
pozwalających programowi "zorientować się" do którego miejsca ma wrócić po zakończeniu
jednego z wywołań rekurencyjnych. Inną częstą wadą rekurencji jest kompletnie niezależne
rozwiązywanie podproblemów, tak, że czasem jeden problem jest rozwiązywany w kilku
miejscach rozwinięcia rekurencji, np. w powyższym przykładzie obliczania
niepotrzebnie
jest dwukrotnie obliczana wartość
Takie problemy nie pojawiają się przy drugim z
przykładów. Niezaprzeczalną zaletą rekurencji jest przejrzystość programów, które z niej
korzystają.
Rekurencja ogonowa
Rekurencja ogonowa, zwana też Tail call lub prawostronną jest rodzajem rekurencji, w
której ostatnia operacja wykonywana przez funkcję to rekurencyjne wywołanie samej siebie
lub zwrócenie końcowego wyniku[1]. Taka funkcja może zostać łatwo zamieniona na iterację,
zarówno ręcznie, jak i automatycznie, co redukuje wielkość stosu oraz zwiększa wydajność.
Ta technika iteracyjnego wykonywania obliczeń jest powszechna w programowaniu
funkcyjnym promującym używanie rekurencji, która w przeciwnym wypadku zajęłaby cały
dostępny stos.
Opis
Wywołanie funkcji z danego kawałka kodu oznacza dla komputera wykonanie skoku w inne
miejsce. Zanim go wykona, musi zapamiętać m.in. adres powrotu, wartości rejestrów oraz
zmiennych. Najczęściej wykorzystywana jest do tego struktura danych zwana stosem, w
której przechowywane są informacje o wszystkich aktualnie wykonywanych funkcjach
zgodnie z kolejnością ich zagnieżdżenia, przy czym każda funkcja zajmuje pojedynczą ramkę
stosu. Każdy program rezerwuje na potrzeby stosu ściśle określoną ilość pamięci, co narzuca
ograniczenie na maksymalną ilość zagnieżdżonych wywołań funkcji. W przypadku
niektórych funkcji ostatnia wykonywana w nich operacja przed zwróceniem wyniku to
wywołanie innej funkcji (w szczególności - siebie samej), bez wykonywania żadnych
dodatkowych obliczeń[2]. W tym przypadku zapamiętywanie adresu powrotu oraz stanu na
stosie jest zbędne, ponieważ nie jest on już potrzebny. Takie wywołanie nazywa się
wywołaniem ogonowym (ang. tail call)[3] i może zostać zoptymalizowane zastąpieniem
wywołania przez zwykłą instrukcję skoku.
Optymalizacja wywołań ogonowych może być stosowana do zwykłych, nierekurencyjnych
funkcji, pozwalając na zaoszczędzenie odrobiny czasu i pamięci, a także zmniejszając
zapotrzebowanie na stos. Funkcje rekurencyjne mogą wywoływać siebie bardzo dużą liczbę
razy, a w szczególnych przypadkach potencjalnie w nieskończoność. Wielkość stosu jest
fizycznie ograniczona z góry przez zasoby komputera. Optymalizacja ogonowa sprawia, że
rekurencyjne wywołanie może korzystać z już istniejącej ramki, przez co zapotrzebowanie na
stos maleje z liniowego O(n) do stałego O(1).