140 likes | 345 Views
Programowanie I. Rekurencja. Wstęp - funkcje i dane rekurencyjne. Definicja: Mówimy, że funkcja lub typ danych są rekurencyjne, jeżeli w ich definicji następuje odwołanie do nich samych.
E N D
Programowanie I Rekurencja
Wstęp - funkcje i dane rekurencyjne • Definicja: Mówimy, że funkcja lub typ danych są rekurencyjne, jeżeli w ich definicji następuje odwołanie do nich samych. • Zwykle rekurencja powstaje z powodu definiowania danej wielkości przez samą siebie, np. znana funkcja silnia może być zdefiniowana matematycznie następująco (najpierw wykonujemy operacje w najbardziej zagnieżdżonych nawiasach): • n*(n-1)*...*1=n*((n-1)*...*1) • Jej odpowiednia definicja programowa jest następująca: • silnia(n)= 1, gdy n=0 • n*silnia(n-1) dla n>0 • Natomiast rekurencyjny typ danych możemy określić następująco: rozważmy zbiór B+ wszystkich niepustych ciągów złożonych z elementów ustalonego alfabetu B. B+ może być określona następująco: • jeśli b B, to <b> B+, • jeśli w B+ i b B, to w<b> B+.
Wstęp • Funkcje rekurencyjne pozwalają wyrazić nam w sposób jawny szczególny rodzaj złożenia czynności – tzw. złożenie rekurencyjne. • Z takim rodzajem złożenia spotkaliśmy się już w semantyce instrukcji “while” i “do”. • Dla instrukcji „while” możemy złożenie rekurencyjne zdefiniować następująco: void dopóki(int B) { if (B) { S; dopóki(B)} }
Wstęp • a dla „do” void powtarzaj(int B) { S; if (B) { powtarzaj(B); } } • Podobnie możemy postąpić dla instrukcji for.
Wstęp • Algorytmy zapisane za pomocą rekurencji są zapisywane bardzo prosto i zapisanie ich za pomocą czynności określanych iteracyjnie nie musi być łatwe. • Przykłady zapisu znanych instrukcji za pomocą ich odpowiedników rekurencyjnych pokazują, że iterację można zapisać za pomocą rekurencji, natomiast odwrotna zasada nie jest prawdziwa. • Przykładem takiej sytuacji mogą być algorytmy, w których realizacja wymaga rozwiązania za pomocą odpowiedniej, zmiennej w zależności od danych, ilości pętli iteracyjnych. • Należy jednak dodać, że pochopne stosowanie rekurencji może powodować poważne kłopoty z pamięcią i należy szczególnie ostrożnie rozważać problem skończoności obliczeń.
Wstęp - rola stopu • Np. procedura silnia, która może być zdefiniowana następująco: int silnia(int n) { if (n=0) silnia=1; else silnia=n*silnia(n-1) } • Co dla n<0?. Czy można to poprawić?
Wstęp - działanie rekurencji • Przykład: void p1(int n) { if (n>0) p1(n-1); cout <<n; } int main() { p1(4); getch(); return 0; } Wywołujemy p1(4), jaki wynik i dlaczego?
Wstęp - działanie rekurencji • Przykład void p2(int n) { cout <<n; if (n>0) p2(n-1); } int main() { p2(4); getch(); return 0; } Wywołujemy p2(4), jaki wynik?
Wstęp - przykłady • Sumujemy do napotkania 0 int suma() { int x; cin>>x; if (x!=0) return suma()+x; else return 0; } int main(int argc, char **argv) { cout<<suma(); getch(); return 0; }
Przykłady - silnia int silnia(int n) { if (n>0) return n*silnia(n-1); else return 1; } int main(int argc, char **argv) { cout<<silnia(4); getch(); return 0; }
Rekurencja - przykład Wzór: fibn=fibn-1+fibn-2, fib1=fib2=1 Implementacja int fib (int n) { if (n < 3 ) return (1); else { return( fib(n-2) + fib(n-1)); }
Rekurencja • Przykład algorytmu, którego nie można zaimplementować za pomocą iteracji: • Problem n-hetmanów: • ustaw n-hetmanów na szachownicy o wymiarze nxn, tak by się wzajemnie nie szachowały. • Idea algorytmu (algorytm z nawrotami): • stawiamy hetmana na wybranym polu, • następne hetmany dostawiamy tak, by się nie szchowały, • jeśli to niemożliwe, to zdejmujemy ostatniego hetmana i powtarzamy dla innych pól. • Przykład działania na tablicy.
Rekurencja • Bardziej formalne rozwiązanie: • Stopniowa konstrukcja polega na tym, że znajdujemy reprezentację rozwiązania x postaci [x1,x2,...,xn] konstruując kolejno [x1], [x1,x2], [x1,x2,x3] itd. w ten sposób, że • 1.każde przejście od [x1,...,xj] do [x1,...,xj,xj+1] jest prostsze niż obliczanie całego próbnego rozwiązania, • 2.jeśli q jest predykatem charakteryzującym rozwiązanie, to musi zachodzić: • j ((1<=j<=n)=>(q(x)=>q([x1,...,xj]) • Warunek 2 oznacza, że aby otrzymać pełne i poprawne rozwiązanie musimy uzupełnić rozwiązanie częściowe tak, by spełniało ono kryterium poprawności. Jeżeli takie uzupełnienie nie jest w danym momencie możliwe, to odwołujemy pewne poprzednie uzupełnienia tego rozwiązania, czyli skracamy je do [x1,...,xi], gdzie i<j i próbujemy inne uzupełnienie. Takie powroty i próbowanie nowego rozwiązania nazywamy nawracaniem. • Implementacja samodzielnie.