Instrukcja laboratoryjna 6 Podstawy programowania 2 Temat: Funkcje i procedury rekurencyjne Przygotował: mgr inż. Tomasz Michno Wstęp teoretyczny Rekurencja (inaczej nazywana rekursją, ang. recursion) oznacza odwoływanie się funkcji do samej siebie. Stosowana jest w matematyce, informatyce i logice. Najłatwiej jej działanie objaśnić na przykładach. Przykład. Algorytm Euklidesa na wyznaczanie największego wspólnego dzielnika (NWD) można zapisać za pomocą matematycznego równania: NWD(a,b)= a NWD(b,amod b) dla b=0 dla b gdzie: a, b liczby,dla których szukamy największego wspólnego dzielnika mod operator dzielenia modulo (reszta z dzielenia) założenie: a>= b Zapis w pseudokodzie mógłby wyglądać następująco: funkcja NWD(a, b : liczby) jeśli(b=0) wtedy zwróć a; jeśli(b >= ) wtedy zwróć NWD(b, a mod b); Ten sam algorytm w wersji iteracyjnej (bez rekurencji): funkcja NWD(a, b : liczby) powtarzaj w pętli dopóki b>0 zapamiętaj w zmiennej tymczasowej wartość zmiennej b b:=a mod b;
a:=zmienna tymczasowa zwróć a; Jak można zauważyć wersja iteracyjna algorytmu jest znacznie dłuższa i bardziej skomplikowana. Prześledźmy teraz działanie funkcji rekurencyjnej dla obliczenia NWD liczb 8 i 6: NWD(8, 6): Nr wywołania Wartość a Wartość b Wykonywane instrukcje 8 6 jeśli(6 >= ) wtedy zwróć NWD(6, 8 mod 6); oznacza to, że wywołujemy ponownie funkcję NWD(6, 2) i zwracamy jej wynik 2 6 2 jeśli(2 >= ) wtedy zwróć NWD(2, 6 mod 2); oznacza to, że wywołujemy ponownie funkcję NWD(2, 0) i zwracamy jej wynik 3 2 0 jeśli(b=0) wtedy zwróć a; funkcja zwraca wartość zmiennej a, czyli 2 Patrząc na tabelkę, można by napisać skrótowo przebieg wykonywanych operacji: NWD(8,6) = NWD(6,2) = NWD(2,0) = 2 Przykład 2 Obliczanie silni. Wzór na silnię można zapisać następująco: silnia(n)= n silnia(n ) dla n=0 dla n Wersja w pseudokodzie: funkcja silnia(n : liczba) jeśli (n=0) wtedy zwróć jeśli (n>=) wtedy zwróć n*silnia(n-)
Przykładowo chcemy obliczyć silnię dla n=3: Nr wywołania Wartość n Wykonywane instrukcje 3 jeśli (3>=) wtedy zwróć 3*silnia(2-) oznacza to wywołanie silnia(2) i pomożenie liczby którą zwróci przez 3 2 2 jeśli (2>=) wtedy zwróć 2*silnia(2-) oznacza to wywołanie silnia() i pomożenie liczby którą zwróci przez 2 3 jeśli (>=) wtedy zwróć *silnia(-) oznacza to wywołanie silnia(0) i pomożenie liczby którą zwróci przez 4 0 jeśli (n=0) wtedy zwróć Zapis w powyższej tabeli jest równoznaczny z zapisem matematycznym: 3!=3*2!=3*2*!=3*2**0!=3*2** funkcja silnia(3) jeśli (3>=) wtedy zwróć 3*silnia(2) 7 3*2=6 wynik=6 funkcja silnia(2) jeśli (2>=) wtedy zwróć 2*silnia() 6 2*=2 funkcja silnia() jeśli (>=) wtedy zwróć *silnia(0) 2 5 *= 3 funkcja silnia(0) jeśli (n=0) wtedy zwróć 4
Powyższy rysunek pokazuje, jak realizowane są wywołania rekurencyjne. Strzałki z czarnymi numerami pokazują kolejność operacji. Zielone liczby informują, jaką wartość zwraca funkcja. Na początku (w silnia(3)) następuje próba zwrócenia wartości 3*silnia(2). Program nie posiada informacji jaką wartość posiada silnia(2), dlatego odkłada na stosie miejsce, do którego ma wrócić i wywołuje funkcję silnia(2). W funkcji silnia(2) jest podobnie program nie posiada informacji na temat funkcji silnia(), dlatego ponownie odkłada na stos miejsce do którego ma wrócić i wywołuje funkcję silnia(). W funkcji silnia() występuje identyczna sytuacja. Po wywołaniu silnia(0) zwracana jest wartość, ponieważ było to ostatnie wywołanie rekurencyjne funkcji. Następnie ze stosu jest zdejmowane miejsce, do którego należy wrócić (silnia()). Obliczana jest wartość funkcji silnia(), czyli *silnia(0)=*. Następnie zdejmowane jest ze stosu kolejne miejsce, do którego należy wrócić (silnia(2)). Obliczana jest wartość silnia(2), czyli 2*silnia()=2*. Następnie kolejny raz jest zdejmowane ze stosu miejsce do którego należy wrócić (silnia(3)). Obliczana jest wartość silnia(3), czyli 3*silnia(2)=3*2=6. Ostatecznie zwracany jest wynik funkcji silnia(3)=6. Zalety i wady rekurencji Najważniejszymi zaletami rekurencji są znaczne skrócenie kodu oraz często łatwiejsze jego napisanie. Najpoważniejszą wadą rekurencji jest jednak duże użycie stosu każde wywołanie rekurencyjne powoduje odłożenie na stos nie tylko miejsca w kodzie, do którego należy wrócić, ale również wszystkich aktualnych wartości zmiennych. Przy wielu wywołaniach może nastąpić przepełnienie sterty i zakończenie programu z błędem. Dlatego należy pamiętać, aby wywołań rekurencyjnych nie było zbyt dużo. Dodatkowo zazwyczaj funkcje rekurencyjne są wolniejsze od ich odpowiedników iteracyjnych. 2 Zadania. Napisz program, który zliczy sumę określonej liczby elementów ciągu arytmetycznego. W tym celu napisz dwie wersje jedną z wykorzystaniem rekurencji i drugą bez niej. Porównaj wyniki obu funkcji oraz długość kodu potrzebnego na ich napisanie. Funkcję można zapisać poniższym wzorem matematycznym: suma(n, k,r)= n dla k= n+suma(n+r, k, r) dla k 2 gdzie: n pierwszy element w ciągu k liczba elementów do obliczenia r różnica ciągu
2. Napisz program, który będzie obliczał n-ty wyraz ciągu Fibonacciego według wzoru: 0 dla n=0 F(n)= dla n= F(n )+F (n 2) dla n> Utwórz dodatkowo wersję bez rekurencji, a następnie oblicz za pomocą obu wersji 20, 25 oraz 30 wyraz ciągu. Zaobserwuj co się dzieje, a następnie spróbuj wyjaśnić przyczyny. Wskazówka: Użyj typu longint, ponieważ liczby mogą wyjść poza zakres zwykłego typu integer. 3. Korzystając z programów z poprzednich laboratoriów, napisz funkcję rekurencyjną, która będzie wyszukiwała element w drzewie. Porównaj ją z wersją bez rekurencji. UWAGA! Pamiętaj o zapisywaniu kodów źródłowych przed uruchomieniem programu.