Rekurencja
Definicja rekurencji: 1. Zakończenie algorytmu jest jasno określone; 2. „Duży” problem = problem elementarny + problem o mniejszym stopniu skomplikowania niż problem początkowy.
Zadanie: Algorytm rekurencyjny rozwiązania: dysponujesz tablicą n liczb całkowitych; należy określić, czy w tablicy występuje liczba x podana jako parametr? Algorytm rekurencyjny rozwiązania: 1. pobierz pierwszy niezbadany element tablicy n-elementowej; 2. jeśli ten element jest równy x, to wypisz “element znaleziono” i zakończ zadanie; 3. w przeciwnym przypadku zbadaj pozostałą część tablicy. (prog. SZUKTAB.CPP)
Podstawowe błędy w programach rekurencyjnych: złe określenie warunku zakończenia programu; niewłaściwa dekompozycja problemu.
Jak wykonują się programy rekurencyjne? Przykład: Napisz program rekurencyjny na obliczenie silni z n: 0! = 1; n! = n*(n-1)!, dla n >= 1 (prog. SILNIA1.CPP)
Ilustracja rekurencyjnego sposobu obliczania 3! x = 0? 3*2! 2*1! 1*0! 1 x=3 x=2 x=1 x=0 nie TAK pionowe strzałki w dół oznaczają zagłębianie się programu z poziomu n na n-1 itd. w celu dotarcia do przypadku elementarnego 0!; pozioma strzałka oznacza obliczanie wyników cząstkowych; ukośna strzałka prezentuje proces przekazywania wyniku cząstkowego z poziomu niższego na wyższy. (plik SILNIA1.CPP)
Niebezpieczeństwa rekurencji: nieskończona liczba wywołań rekurencyjnych; wielokrotne wykonanie identycznych obliczeń; przepełnienie stosu; (prog. PULAPKA.CPP)
Ciąg Fibonacciego: fib(0) = 1, fib(1) = 1, fib(n) = fib(n-1) + fib(n-2), gdzie n 2. Zadanie polega na napisaniu programu generującego elementy ciągu Fibonacciego. Ciąg Fibonacciego jest używany do wielu różnych i czsami zaskakujących celów. Program FIB.CPP generuje liczby Fibonacciego. Spróbujmy prześledzić dokładnie wywołania rekurencyjne dla n = 4. Nieskomplikowana analiza prowadzi do następującego drzewa przedstawionego na rysunku poniżej:
Wywołania rekurencyjne funkcji Fibonacciego dla n = 4 Każdy zacieniowany prostokąt symbolizuje problem elementarny, natomiast problem o rozmiarze n 2 zostaje rozbity na dwa problemy o mniejszym stopniu skomplikowania: n-1 i n-2. Można łatwo dostrzec, że znaczna część obliczeń jest wykonywana wielokrotnie, np. cała gałąź zaczynająca się od fib(2) jest zdublowana. Funkcja Fib nie ma żadnej możliwości, aby to zauważyć. (Patrz program FIB.CPP). fib(0) = 1, fib(1) = 1, fib(n) = fib(n-1) + fib(n-2), gdzie n 2.
Stack overflow! Przykład: funkcja Mac Carty’ego: Stack overflow, czyli przepełnienie stosu. Jak wykazuje praktyka programistyczna, zdarza nam często zawiesić komputer za pomocą własnego programu, przy czym przez zawieszenie komputera rozumiemy stan, w którym program nie reaguje na nic i trzeba wykonać restart systemu operacyjnego. Sytuacja taka zdarza się nawet najbardziej uważnym programistom i raczej stanowi nieodłączny element pracy programistycznej. Istnieje kilka typowych przyczyn zawieszania programów: zachwianie równowagi systemu operacyjnego przez nielegalne użycie jego zasobów; nieskończone pętle; brak pamięci; nieprawidłowe lub niejasne określenie warunków zakończenia programu; błąd programowania, np. zbyt wolno wykonujący się algorytm. Programy rekurencyjne są zazwyczaj dość pamięciochłonne: z każdym wywołaniem rekurencyjnym wiąże się konieczność zachowania pewnych informacji niezbędnych do odtworzenia stanu sprzed wywołania, a to wymaga określonej pamięci. Dla pewnych programów rekurencyjnych określenie maksymalnego poziomu zagłębienia rekurencji podczas ich wykonywania jest dość łatwe. Analizując program obliczający silnię 3! widzimy od razu, że wywoła on sam siebie tylko 3 razy. W przypadku funkcji Fibonacciego szybka “diagnoza” nie przynosi już tak kompletnej informacji. Przybliżone szacunki nie zawsze należą do najprostszych. Dowodzi tego funkcja MacCarthy’ego, przedstawiona poniżej:
Funkcja Mac Carty’ego: Jak przedstawia się liczba wywołań funkcji MC(x) w zależności od parametru x podanego w wywołaniu? Chyba niewielu byłoby w stanie od razu powiedzieć, że zależność ta ma postać przedstawioną na wykresie powyżej. (Patrz program MC.CPP)
Jeszcze raz o myśleniu rekurencyjnym Narysuj spiralę: delta dl Pomimo oczywistych przykładów na to, że rekurencja jest dla człowieka czymś jak najbardziej naturalnym, niektórzy mają pewne trudności z używaniem jej podczas programowania. Nieumiejętność “wyczucia” istoty tej techniki może wynikać z braku dobrych i poglądowych przykładów na jej wykorzystanie. Idąc za tym stwierdzeniem, przytaczam kilka prostych programów rekurencyjnych, generujących znane motywy graficzne. Zastanówmy się, jak można narysować rekurencyjnie powyższy rysunek.
idea rysowania rekurencyjnego: (x, y) (x’, y’) Parametrami programu są: odstęp pomiędzy liniami równoległymi delta; długość boku rysowanego w pierwszej kolejności dl. Istota rekurencji polega na znalezieniu właściwej dekompozycji problemu. Rekurencyjność naszego zadania jest oczywista, bowiem program zajmuje się powtarzaniem głównie tych samych czynności - rysuje linie pionowe i poziome, jednakże o różnej długości. Naszym zadaniem będzie odszukanie schematu rekurencyjnego i warunków zakończenia procesu wywołań rekurencyjnych. Wybierzmy jako punkt startowy pewną parę (x,y). Idea rozwiązania polega na narysowaniu 4 odcinków “zewnętrznych” spirali i dotarciu do punktu (x’,y’). W tym nowym punkcie startowym możemy już wywołać rekurencyjnie procedurę rysowania, obarczoną oczywiście pewnymi warunkami gwarantującymi jej poprawne zakończenie. (Patrz program SPIRALA.CPP). (prog. SPIRALA.CPP)
Kwadraty parzyste: (prog. KWADRATY.CPP) Rozpatrzmy problem kreślenia rekurencyjnego kwadratów “parzystych” przedstawionych na poniższym rysunku: Przypadkiem elementarnym będzie tutaj narysowanie jednej pary kwadratów, przy czym wewnętrzny jest obrócony w stosunku do zewnętrznego. Więc główny problem będzie polegał na wyborze właściwego miejsca wywołania rekurencyjnego. Poniżej przedstawiono program realizujący to zadanie. (Patrz program KWADRATY.CPP). (prog. KWADRATY.CPP)
Fraktal trókątny (prog FRAKT_TR.CPP) n - stopień fraktala L - długość boku 1. Jeśli n = 0, to zakończ algorytm; 2. Jeśli n > 0, to powtórz 3 razy: - narysuj bok trójkąta o długości L; - wykonaj obrót w prawo o 60; - narysuj fraktal trójkątny stopnia n-1 o boku L/2; - wykonaj obrót w prawo o 60. (prog FRAKT_TR.CPP)
Fraktal sześciokątny (prog. FRAKTAL6.CPP) n - stopień fraktala L - długość boku 1. Jeśli n = 0, to zakończ algorytm; 2. Jeśli n > 0, to powtórz 6 razy: - narysuj bok sześciokąta foremnego o długości L; - narysuj fraktal sześciokątny stopnia n-1 o boku L/2; - wykonaj obrót w prawo o 60. (prog. FRAKTAL6.CPP)
Fraktal gwiaździsty (prog. GWIAZDY.CPP) Jeśli n = 0, to zakończ algorytm; Przypisz: alfa = 3*360/7; Jeśli n > 0, to powtórz 7 razy: - narysuj bok ramienia gwiazdy o długości L; - wykonaj obrót w prawo o k*alfa stopni; - narysuj fraktal gwiaździsty stopnia n-1 o boku L/2; - wykonaj obrót w prawo o (1-k)*alfa stopni. n - stopień fraktala L - długość boku alfa - kąt obrotu k - współczynnik obrotu (prog. GWIAZDY.CPP)
Wieże Hanoi A B C
Wieże Hanoi A B C A B C a) A B C b) A B C c) A B C d) A B C e) A B C f) A B C g)
funkcja PRZENIEŚ (A, B, C, n): Wieże Hanoi funkcja PRZENIEŚ (A, B, C, n): jeśli n = 1 to PRZENIEŚ (A, C) w przeciwnym przypadku { PRZENIEŚ (A,C, B, n-1); PRZENIEŚ (A, C); PRZENIEŚ (B, A, C, n-1); } Zadanie - pozornie dość skomplikowane - daje się rozwiązać w zdumiewająco prosty sposób. Istotnie, zauważmy najpierw, że gdy n=1, zadanie sprowadza się do rozwiązania PRZENIEŚ(Start, Cel). W ogólnym przypadku rozwiązanie wyznaczy procedura przenieś(A,B,C,n). Ponieważ znamy treść procedury w przypadku n=1, będziemy ją budować z n krążków, zakładając, że znana ona jest dla n-1 krążków: przenieś n-1 krążków z A na B; przenieś 1 krążek z A na C; przenieś n-1 krążków z B na C. (prog. HANOI.CPP)
Programy demonstracyjne: SZUKTAB.CPP SILNIA1.CPP PULAPKA.CPP FIB.CPP MC.CPP SPIRALA.CPP KWADRATY.CPP FRAKT_TR.CPP FRALTAL6.CPP GWIAZDY.CPP PL_KOCHA.CPP HANOI.CPP