to get the file

Transkrypt

to get the file
Instrukcja
Podstawy programowania 2
laboratoryjna
Temat: Funkcje i procedury rekurencyjne
6
Przygotował: mgr inż. Tomasz Michno
1 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 1.
Algorytm Euklidesa na wyznaczanie największego wspólnego dzielnika (NWD) można zapisać za
pomocą matematycznego równania:
NWD(a , b)=
gdzie:
{
a
NWD(b , a mod b)
dla b=0
dla b⩾1
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 >= 1) 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
1
Wartość Wartość
a
b
8
6
Wykonywane instrukcje
jeśli(6 >= 1) 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 >= 1) 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)=
{
1
n⋅silnia(n−1)
dla n=0
dla n⩾1
Wersja w pseudokodzie:
funkcja silnia(n : liczba){
jeśli (n=0) wtedy zwróć 1
jeśli (n>=1) wtedy zwróć n*silnia(n-1)
}
Przykładowo chcemy obliczyć silnię dla n=3:
Nr
Wartość n
wywołania
1
Wykonywane instrukcje
jeśli (3>=1) wtedy zwróć 3*silnia(2-1)
3
oznacza to wywołanie silnia(2) i pomożenie liczby którą zwróci
przez 3
2
jeśli (2>=1) wtedy zwróć 2*silnia(2-1)
2
oznacza to wywołanie silnia(1) i pomożenie liczby którą zwróci
przez 2
3
jeśli (1>=1) wtedy zwróć 1*silnia(1-1)
1
oznacza to wywołanie silnia(0) i pomożenie liczby którą zwróci
przez 1
4
jeśli (n=0) wtedy zwróć 1
0
Zapis w powyższej tabeli jest równoznaczny z zapisem matematycznym:
3!=3*2!=3*2*1!=3*2*1*0!=3*2*1*1
3*2=6
funkcja silnia(3){
jeśli (3>=1) wtedy zwróć 3*silnia(2)
}
wynik=6
7
1
2*1=2
funkcja silnia(2){
jeśli (2>=1) wtedy zwróć 2*silnia(1)
}
6
2
1*1=1
funkcja silnia(1){
jeśli (1>=1) wtedy zwróć 1*silnia(0)
}
5
3
1
funkcja silnia(0){
jeśli (n=0) wtedy zwróć 1
}
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(1), dlatego ponownie odkłada na stos miejsce do którego ma wrócić i wywołuje
funkcję silnia(1). W funkcji silnia(1) występuje identyczna sytuacja. Po wywołaniu silnia(0)
zwracana jest wartość 1, ponieważ było to ostatnie wywołanie rekurencyjne funkcji. Następnie
ze stosu jest zdejmowane miejsce, do którego należy wrócić (silnia(1)). Obliczana jest wartość
funkcji silnia(1), czyli 1*silnia(0)=1*1. 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(1)=2*1.
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
1. 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)=
gdzie:
{
n
n+suma(n+r , k−1, r)
n – pierwszy element w ciągu
k – liczba elementów do obliczenia
r – różnica ciągu
dla k=1
dla k⩾2
{
2. Napisz program, który będzie obliczał n-ty wyraz ciągu Fibonacciego według wzoru:
F(n)=
0
1
F(n−1)+F (n−2)
dla n=0
dla n=1
dla n>1
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.