Na czym polega rekurencja?

Istotą rekurencjirekurencjarekurencji jest wykorzystanie funkcji, która – aby rozwiązać pewien problem – wywołuje samą siebie, aż do momentu, gdy zadanie zostanie wykonane. Funkcja ta może być typu voidfunkcja typu voidvoid, gdy wywoływana funkcja nie zwraca wartości do funkcji wywołującej, lecz np. wypisuje ją do konsoli. Najczęściej jednak funkcja rekurencyjna oblicza oraz zwraca wartość.

Aby mechanizm rekurencji działał poprawnie, potrzebny jest przynajmniej jeden warunek, po spełnieniu którego funkcja nie zostanie wywołana po raz kolejny, lecz zakończy działanie (a także zwróci wartość – o ile miała ją zwracać). Jeżeli taki warunek się nie pojawi, funkcja będzie wywoływać się w nieskończoność – może to spowodować zawieszenie programu – jak w przypadku nieskończonej pętlinieskończona pętlanieskończonej pętli – lub przepełnienie stosu. Nie otrzymamy wówczas rozwiązania problemu.

Z każdym kolejnym wywołaniem funkcji rekurencyjnej przynajmniej jeden z jej argumentówargument funkcjiargumentów musi zmienić wartość. Zmiany argumentów muszą prowadzić do przypadku bazowego. Wartości argumentów nie mogą powtórzyć się w następnych wywołaniach, z wyjątkiem argumentów, których wartości przerywają dany ciąg wywołań rekurencyjnych. Wartości argumentów stanowią wtedy przypadek podstawowy rekurencji. Jeżeli złamiemy chociaż jedną z przedstawionych zasad, funkcja rekurencyjna będzie wywoływać się bez końca. Kolejne wywołanie zostaje zapisane na stosie, więc gdy funkcja będzie uruchamiana w nieskończoność, w pewnym momencie stos przepełni się, a program zakończy działanie.

Każde rekurencyjne wywołanie funkcji zatrzymuje wykonywanie funkcji bieżącej. Czeka ona, aż funkcja przez nią wywołana zakończy działanie i zwróci wartość. Przy wywoływaniu kolejnych funkcji rekurencyjnych poprzednie funkcje także zatrzymują się i czekają, aż funkcje przez nie wywołane zwrócą obliczoną wartość.

Opisany proces można przedstawić graficznie przy użyciu drzewa wywołań rekurencyjnych.

R8UR63QhQDMVT
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Z przedstawionej grafiki wynika, że pierwszym wywołaniem funkcji rekurencyjnej Funkcja było: Funkcja(a). Ta z kolei w pierwszej kolejności wywołała rekurencyjnie Funkcja(b). A zatem wywołanie Funkcja(a) czeka, aż wywołana Funkcja(b) zwróci wartość.

Kolejnym krokiem było wywołanie Funkcja(c). Wywołanie funkcji z takim argumentem nie spowodowało kolejnego wywołania rekurencyjnego, lecz zwrócenie wartości. Zobrazowane to zostało przy użyciu czerwonej strzałki z oznaczeniem 3. Jednak Funkcja(b) wywołała nie jedno, lecz dwa wykonania rekurencyjne. Drugim jest Funkcja(d), które podobnie jak Funkcja(c), zamiast wywoływać dalsze wykonania rekurencyjne, zwróciła wartość.

Kolejny krok przedstawia czerwona strzałka z numerem 6. Wywołanie Funkcja(b) zwraca wartość do Funkcja(a). Następnym krokiem jest wywołanie kolejno: Funkcja(e)Funkcja(f)Funkcja(g), po czym ostatnie wywołanie zwraca wartość do poprzedniego i w ten sposób dochodzimy do pierwszego wywołania Funkcja(a), które finalnie zwraca wartość.

Rekurencja – przykład

Zanim przejdziemy do ciągu Fibonacciego, napiszmy prosty program wykorzystujący mechanizm rekurencji. Program ten zwróci sumę wszystkich liczb naturalnych mniejszych lub równych liczbie podanej jako argument.

Rozpoczynamy od zdeklarowania funkcji.

Linia 1. public static int sumaLiczbNaturalnych otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. zamknij nawias klamrowy.

Ponieważ funkcja ma obliczyć sumę liczb naturalnych, typem zwracanych danych jest int. Parametr n będzie górną granicą składników sumy.

Zaczniemy od zapisania warunku, po spełnieniu którego funkcja zakończy działanie i zwróci wartość. Warunek będzie dotyczył najmniejszej możliwej wartości parametru n. Zakładamy, że liczby naturalne zaczynają się od 1. Ile wynosi suma liczb naturalnych mniejszych lub równych 1? Jest to 1. Skonstruujmy więc instrukcję warunkowąinstrukcja warunkowainstrukcję warunkową, która będzie odpowiedzialna za działanie programu w przypadku, gdy wartość n wynosi 1:

Linia 1. public static int sumaLiczbNaturalnych otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. if otwórz nawias okrągły n znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. return 1 średnik. Linia 4. zamknij nawias klamrowy. Linia 5. zamknij nawias klamrowy.

Kolejnym i ostatnim krokiem jest rekurencyjne wywołanie funkcji.

Linia 1. public static int sumaLiczbNaturalnych otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. if otwórz nawias okrągły n znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. return 1 średnik. Linia 4. zamknij nawias klamrowy. Linia 6. return n plus sumaLiczbNaturalnych otwórz nawias okrągły n minus 1 zamknij nawias okrągły średnik. Linia 7. zamknij nawias klamrowy.

Ponieważ chcemy uzyskać sumę liczb naturalnych, a różnica między kolejnymi liczbami naturalnymi zawsze wynosi 1, za każdym razem, gdy wywołujemy kolejną funkcję, odejmujemy od jej argumentu wartość 1. Do wyniku tego wywołania dodajemy wartość aktualnego argumentu i ta suma zostaje przekazana funkcji, która wywołała bieżącą funkcję.

Aby lepiej zrozumieć działanie programu, sprawdźmy, jak się on zachowa, gdy jako argument podamy wartość 3.

Linia 1. public static int sumaLiczbNaturalnych otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. prawy ukośnik prawy ukośnik n znak równości 3. Linia 4. if otwórz nawias okrągły 3 znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 8. return 3 plus sumaLiczbNaturalnych otwórz nawias okrągły 2 zamknij nawias okrągły średnik. Linia 9. zamknij nawias klamrowy.

Sprawdzamy, czy 3 jest równe 1. Nie jest to prawdą, więc pomijamy instrukcję warunkową i przechodzimy do wywołania rekurencyjnego. Liczbę 3 dodajemy do wartości zwróconej przez sumaLiczbNaturalnych(2). Funkcja przerywa działanie i przechodzi do funkcji, którą sama wywołała, czyli sumaLiczbNaturalnych(2).

Linia 1. public static int sumaLiczbNaturalnych otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. prawy ukośnik prawy ukośnik n znak równości 2. Linia 4. if otwórz nawias okrągły 2 znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 8. return 2 plus sumaLiczbNaturalnych otwórz nawias okrągły 1 zamknij nawias okrągły średnik. Linia 9. zamknij nawias klamrowy.

Jak wiemy, 2 również nie jest równe 1. Pomijamy warunek i ponownie zatrzymujemy wykonywanie tej funkcji. Zwróćmy uwagę na funkcję, która została wywołana, czyli sumaLiczbNaturalnych(1).

Linia 1. public static int sumaLiczbNaturalnych otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. prawy ukośnik prawy ukośnik n znak równości 1. Linia 4. if otwórz nawias okrągły 1 znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 7. zamknij nawias klamrowy.

W tym przypadku instrukcja warunkowa nie zostanie pominięta. Skoro 1 == 1, nie wywołujemy już funkcji sumaLiczbNaturalnych(), lecz zwracamy wartość 1. Przekazujemy ją funkcji, która wywołała bieżącą funkcję.

Linia 1. public static int sumaLiczbNaturalnych otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. prawy ukośnik prawy ukośnik n znak równości 2. Linia 4. if otwórz nawias okrągły 2 znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 8. return 2 plus 1 średnik. Linia 9. zamknij nawias klamrowy.

Ta z kolei otrzymała wartość, na którą czekała. Może więc dodać ją do swojego argumentu i zwrócić obliczony wynik funkcji, która wywołała tę wartość.

Linia 1. public static int sumaLiczbNaturalnych otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. prawy ukośnik prawy ukośnik n znak równości 3. Linia 4. if otwórz nawias okrągły 3 znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 8. return 3 plus 3 średnik. Linia 9. zamknij nawias klamrowy.

Wywołana pierwotnie funkcja zwraca wartość 6, która jest sumą wszystkich liczb naturalnych mniejszych lub równych 3.

Ciąg Fibonacciego

Definicja ciągu Fibonacciego wygląda następująco: początkowymi elementami ciągu są 0 oraz 1, natomiast każdy kolejny element jest sumą dwóch poprzednich. Trzecim elementem ciągu Fibonacciego będzie zatem , czwartym , piątym  itd. Indeksy elementów zaczynają się od 0, podobnie jak indeksy tablicy w języku Java. Pierwszy element – o wartości 0 – ma indeks n = 0.

Rekurencja

1
Problem 1

Napisz w języku Java program, który zwróci wyraz ciągu Fibonacciego o indeksie n. Zastosuj mechanizm rekurencyjny. Przetestuj jego działanie dla n = 3.

Ciąg Fibonacciego to ciąg liczb, w którym pierwszy wyraz jest równy 0, drugi jest równy 1, a każdy następny jest sumą dwóch poprzednich. Ciąg ten można przedstawić za pomocą rekurencyjnego wzoru funkcji , obliczającej  element ciągu Fibonacciego:

F n = { 0 dla  n = 0 1 dla  n = 1 F n 1 + F n 2 dla  n > 1

Specyfikacja:

Dane:

  • n – indeks elementu ciągu; liczba naturalna

Wynik:

Program oblicza wyraz ciągu Fibonacciego o indeksie n.

R1O3xs3FbNTuX
Wybierz jedno nowe słowo poznane podczas dzisiejszej lekcji i ułóż z nim zdanie.
1
Polecenie 1

Porównaj swoje rozwiązanie z prezentacją.

Rb0Jly4fZPrhF1
Wysłuchaj nagrania abstraktu i zastanów się, czego jeszcze chciałbyś się dowiedzieć w związku z tematem lekcji.
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Iteracja

Napiszemy program, który zwróci element ciągu Fibonacciego o indeksie n. Zastosujemy iterację. Zaczniemy ponownie od utworzenia funkcji.

Linia 1. public static long iterFibonacci otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. zamknij nawias klamrowy.

Wybieramy typ long, ponieważ wartości kolejnych wyrazów ciągu Fibonacciego rosną wykładniczo.

Parametr n funkcji będzie indeksem elementu ciągu, który ma być obliczony.

Zapisujemy warunki, po których spełnieniu zostanie zatrzymane działanie funkcji i zwróci ona wartość. Wiemy, że pierwszy wyraz ciągu Fibonacciego – zatem wyraz o indeksie 0 – ma wartość 0. Natomiast drugi wyraz – czyli wyraz o indeksie 1 – wynosi 1. Zapiszemy odpowiednie instrukcje warunkowe.

Linia 1. public static long iterFibonacci otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. if otwórz nawias okrągły n znak równości znak równości 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. return 0 średnik. Linia 4. zamknij nawias klamrowy else if otwórz nawias okrągły n znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 7. zamknij nawias klamrowy.

Musimy brać również pod uwagę przypadek, gdy wartość parametru n jest większa niż 1. W takiej sytuacji każdy kolejny element jest sumą dwóch poprzednich. Dla dalszego działania funkcji potrzebujemy zmiennych pomocniczych. Inicjalizujemy trzy zmienne pomocnicze: a, b (oznaczające wartości dwóch początkowych wyrazów ciągu) oraz i (indeks kolejnego elementu ciągu Fibonacciego). Przypisujemy im odpowiednio wartości 0, 1 i 2.

Linia 1. public static long iterFibonacci otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. if otwórz nawias okrągły n znak równości znak równości 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. return 0 średnik. Linia 4. zamknij nawias klamrowy else if otwórz nawias okrągły n znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 8. long a znak równości 0 średnik. Linia 9. long b znak równości 1 średnik. Linia 10. int i znak równości 2 średnik. Linia 12. zamknij nawias klamrowy.

Zapisujemy pętlę while, która będzie wykonywać się tak długo, dopóki wartość zmiennej i jest mniejsza od wartości zmiennej n lub jej równa.

Linia 1. public static long iterFibonacci otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. if otwórz nawias okrągły n znak równości znak równości 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. return 0 średnik. Linia 4. zamknij nawias klamrowy else if otwórz nawias okrągły n znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 8. long a znak równości 0 średnik. Linia 9. long b znak równości 1 średnik. Linia 10. int i znak równości 2 średnik. Linia 12. while otwórz nawias okrągły i otwórz nawias ostrokątny znak równości n zamknij nawias okrągły otwórz nawias klamrowy. Linia 14. zamknij nawias klamrowy. Linia 16. zamknij nawias klamrowy.

W każdym obiegu pętli while najpierw obliczamy i-ty wyraz ciągu, czyli sumę dwóch poprzednich wyrazów ciągu. Wynik przypisujemy do zmiennej suma. Następnie zmiennej a przypisujemy wartość zmiennej b, a zmiennej b wartość zmiennej suma. Na końcu zwiększamy wartość zmiennej i o 1.

Linia 1. public static long iterFibonacci otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. if otwórz nawias okrągły n znak równości znak równości 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. return 0 średnik. Linia 4. zamknij nawias klamrowy else if otwórz nawias okrągły n znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 8. long a znak równości 0 średnik. Linia 9. long b znak równości 1 średnik. Linia 10. int i znak równości 2 średnik. Linia 12. while otwórz nawias okrągły i otwórz nawias ostrokątny znak równości n zamknij nawias okrągły otwórz nawias klamrowy. Linia 13. long suma znak równości a plus b średnik. Linia 14. a znak równości b średnik. Linia 15. b znak równości suma średnik. Linia 16. i znak równości i plus 1 średnik. Linia 18. zamknij nawias klamrowy. Linia 20. zamknij nawias klamrowy.

Po zakończeniu pętli wartość zmiennej jest zwracana jako wynik działania funkcji.

Linia 1. public static long iterFibonacci otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 2. if otwórz nawias okrągły n znak równości znak równości 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. return 0 średnik. Linia 4. zamknij nawias klamrowy else if otwórz nawias okrągły n znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 5. return 1 średnik. Linia 6. zamknij nawias klamrowy. Linia 8. long a znak równości 0 średnik. Linia 9. long b znak równości 1 średnik. Linia 10. int i znak równości 2 średnik. Linia 12. while otwórz nawias okrągły i otwórz nawias ostrokątny znak równości n zamknij nawias okrągły otwórz nawias klamrowy. Linia 13. long suma znak równości a plus b średnik. Linia 14. a znak równości b średnik. Linia 15. b znak równości suma średnik. Linia 16. i znak równości i plus 1 średnik. Linia 18. zamknij nawias klamrowy. Linia 20. return b średnik. Linia 22. zamknij nawias klamrowy.

Oto cały kod programu:

Linia 1. public class Main otwórz nawias klamrowy. Linia 2. public static void main otwórz nawias okrągły String args otwórz nawias kwadratowy zamknij nawias kwadratowy zamknij nawias okrągły otwórz nawias klamrowy. Linia 3. System kropka out kropka println otwórz nawias okrągły iterFibonacci otwórz nawias okrągły 3 zamknij nawias okrągły zamknij nawias okrągły średnik. Linia 4. zamknij nawias klamrowy. Linia 6. public static long iterFibonacci otwórz nawias okrągły int n zamknij nawias okrągły otwórz nawias klamrowy. Linia 7. if otwórz nawias okrągły n znak równości znak równości 0 zamknij nawias okrągły otwórz nawias klamrowy. Linia 8. return 0 średnik. Linia 9. zamknij nawias klamrowy else if otwórz nawias okrągły n znak równości znak równości 1 zamknij nawias okrągły otwórz nawias klamrowy. Linia 10. return 1 średnik. Linia 11. zamknij nawias klamrowy. Linia 13. long a znak równości 0 średnik. Linia 14. long b znak równości 1 średnik. Linia 15. int i znak równości 2 średnik. Linia 17. while otwórz nawias okrągły i otwórz nawias ostrokątny znak równości n zamknij nawias okrągły otwórz nawias klamrowy. Linia 18. long suma znak równości a plus b średnik. Linia 19. a znak równości b średnik. Linia 20. b znak równości suma średnik. Linia 21. i znak równości i plus 1 średnik. Linia 22. zamknij nawias klamrowy. Linia 23. return b średnik. Linia 24. zamknij nawias klamrowy. Linia 25. zamknij nawias klamrowy.

Słownik

argument funkcji
argument funkcji

element składni w określonym języku programowania, który w wyniku wywołania podprogramu zostaje utożsamiony (skojarzony) z określonym parametrem podprogramu

funkcja typu void
funkcja typu void

funkcja, która nie zwraca żadnej wartości

instrukcja warunkowa
instrukcja warunkowa

element języka programowania sterujący działaniem programu; pozwala wykonać różne instrukcje w zależności od tego, czy zdefiniowane przez programistę wyrażenie logiczne jest prawdziwe czy nieprawdziwe

nieskończona pętla
nieskończona pętla

pętla, która nigdy nie spełnia warunku zakończenia; pętla działająca w nieskończoność

parametr funkcji
parametr funkcji

element składni w określonym języku programowania; umożliwia komunikację pomiędzy podprogramem (funkcją) wywołanym a programem wywołującym; parametry określa się w nagłówku podprogramu (przy jego definicji)

rekurencja
rekurencja

sytuacja, w której funkcja wywołuje samą siebie, aż do napotkania przypadku podstawowego