Z rekurencją mamy do czynienia wtedy, gdy metoda wywołuje samą siebie. Oczywiście, jeśli nie chcemy zawiesić programu, należy umieścić w kodzie jakiś warunek takiego wywołania tak, aby w pewnym momencie doszło do zakończenia ciągu wywołań. Działanie rekurencji pokażę na przykładzie programu obliczającego silnię.
Przykład
Aby pokazać sposób działania rekurencji napiszemy program, który pobierze od użytkownika liczbę całkowitą i obliczy dla niej silnię.
Tak naprawdę, aby obliczyć silnię nie trzeba odwoływać się do rekurencji, można to zrobić za pomocą zwykłej pętli, ale z powodów dydaktycznych nie pójdziemy na łatwiznę.
Przypominam, że silnia (oznaczana znakiem !) to iloczyn kolejnych liczb od 1 do liczby dla której oblicza się silnię, przy czym dla 0 silnia = 1. Na przykład:
5! = 1 * 2 * 3 * 4 * 5 = 120
Ponieważ mnożenie jest przemienne, równie dobrze można powyższe zapisać tak:
5! = 5 * 4 * 3 * 2 * 1 = 120
Mnożenie przez 1 nie zmienia wartości wyniku, więc można wzór nieco uprościć:
5! = 5 * 4 * 3 * 2 = 120
ale dla przejrzystości kodu zignorujemy tą możliwość.
Bardziej ogólnie obliczanie silni można przedstawić tak:
n! = n * n-1 * n-2 ... * 1
Teraz zastanówmy się jak powinien działać program, który przeprowadzi takie obliczenia.
Najpierw należy pobrać od użytkownika liczbę dla której będzie liczona silnia, powinna to być liczba całkowita mieszcząca się w zakresie od 0 do 13. Dolna granica wynika z definicji silni, można ją liczyć dla liczb naturalnych (nieujemnych, całkowitych). Górna z zakresu liczb, które można zapisać w zmiennych typu int
. Możesz ten zakres zwiększyć zmieniając typ używanych w programie zmiennych na long
lub BigInteger
, w drugim przypadku będzie też trzeba dokonać dodatkowych zmian w kodzie odpowiedzialnym za przeprowadzanie obliczeń (zob. poprzedni wpis). Kod odpowiedzialny za pobranie liczby od użytkownika i sprawdzenie jej poprawności umieścimy w osobnej metodzie.
Osobna metoda będzie odpowiadać za obliczenia, przy czym pokażę dwa sposoby rekurencyjnego rozwiązania problemu, oba znajdują się w osobnych metodach (obliczSilnie1
i obliczSilnie2
) w kodzie zamieszczonym poniżej. Dla porównania dołożyłem też przykładowe rozwiązanie za pomocą pętli (obliczSilniePetla
) Zaznaczam, że można poniższy kod znacznie bardziej skondensować (spróbuj w ramach treningu) ale w tej formie, moim zdaniem, bardziej czytelnie pokazuje sposób działania rekurencji.
Rozwiązanie 1 – metoda obliczSilnie1(int liczba, int l)
W tym rozwiązaniu, metoda wywołuje sama siebie za każdym razem wykonując obliczenia, aż dojdzie do ostatniego mnożenia, wtedy wynik jest przekazywany „w górę”.
- Jeśli przekazana liczba równa jest 0, zwracana jest wartość 1, jeśli nie to wykonywane są dalsze kroki:
- Metoda mnoży „bieżący” wynik przez kolejną liczbę z szeregu. Przy pierwszym wywołaniu bieżący wynik jest równy liczbie podanej przez użytkownika a liczba przez którą będzie mnożony jest o jeden mniejsza. Obie liczby podajemy jako argumenty.
- Metoda oblicza bieżący wynik mnożąc obie liczby przez siebie, po czym wywołuje samą siebie, podając tym razem jako argumenty bieżący wynik oraz liczbę, przez którą mnożyliśmy zmniejszoną o 1.
- Wywołana metoda znów dokonuje opisane powyżej operacje i proces tego coraz bardziej zagnieżdżonego wywoływania samej siebie trwa dopóki liczba przez którą zamierzamy mnożyć bieżący wynik jest większa niż 1.
- Kiedy ten próg zostaje osiągnięty, metoda zwraca bieżący wynik, który „wędruje w górę”, czyli do metody, która ją wywołała, ta z kolei znów zwraca ją „wyżej” aż wynik zostaje zwrócony przez pierwsze wywołanie metody w metodzie
main
.
Rozwiązanie 2 – metoda obliczSilnie2(int liczba)
Jest to sposób bardziej „klasyczny”. W tym przypadku najpierw argument za każdym wywołaniem metody zmniejszany jest o 1 aż do chwili, gdy osiągnie wartość 0, wtedy wynik jest zwracany. W metodzie, która otrzymała rezultat dokonywane są obliczenia, których wynik dalej wędruje „w górę”.
- Metoda przyjmuje tylko jeden argument, przy pierwszym wywołaniu jest to podana przez użytkownika liczba
- Jeśli przekazana liczba równa jest 0, zwracana jest wartość 1, jeśli nie to wykonywane są dalsze kroki:
- Metoda wywołuje samą siebie, podając jako argument liczbę o 1 mniejszą niż liczba, która została jej przekazana.
- Rekurencyjne wywoływanie metody kończy się, gdy argument osiągnie wartość 0, wtedy bieżący wynik, który w tym momencie wynosi 1, zostaje przekazany „w górę”
- Zwracana liczba jest mnożona przez argument, który został przekazany do metody, wynik jest przekazany „w górę”
- W końcu wynik końcowy zostaje zwrócony do metody
main
, która wypisuje go na ekranie.
W poniższym kodzie umieściłem też komendy wypisujące bieżące wartości obliczane przez metody a także przekazywane do nich argumenty. Prześledzenie ich wartości oraz kolejności w jakiej są drukowane ułatwia zrozumienie całego procesu.
Dla porównania podaję też rozwiązanie za pomocą pętli for
.
Uwaga: Rekurencja łączy się niestety z dość dużym apetytem na pamięć komputera, nie należy zatem przesadzać z liczbą wywołań rekurencyjnych metody, ponieważ może to spowodować błąd przepełnienia stosu (błąd: java.lang.StackOverflowError
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
import java.util.Scanner; public class Silnie { public static void main(String[] args) { // pobranie od uzytkownika liczby, dla której będzie liczona silnia int l = pobierzLiczbe(); // Pierwszy sposób za pomoca rekurencji System.out.println("Silnia Rekurencja1: " + obliczSilnie1(l, l-1)); // Drugi sposób za pomoca rekurencji System.out.println("Silnia Rekurencja2: " + obliczSilnie2(l)); // Rozwiązanie za pomocą pętli System.out.println("Silnia Petla: " + obliczSilniePetla(l)); } /** * pobiera liczbę od użytkownika */ static int pobierzLiczbe() { // metoda odpowiedzialna za pobranie informacji od użytkownika Scanner skaner = new Scanner(System.in); int liczba; do { System.out.print("Podaj liczbę całkowitą, między 0 i 12: "); liczba = skaner.nextInt(); } while (liczba < 0 || liczba > 12); return liczba; } /** * oblicza silnię */ static int obliczSilnie1(int liczba, int l) { // dla 0 silnia = 1 if (liczba == 0) { return 1; } else { // drukowanie bieżących wartości liczby i l // liczba - bieżąca wartość obliczanej silni // l - kolejna liczba używana w obliczeniach System.out.println(" l: " + l + " Liczba: " + liczba); // obliczanie iloczynu: bieżąca wartość * kolejna liczba liczba = liczba * l; // wywołanie rekurencyjne metody dopóki l > 1 // kolejna liczba w iloczynie jest o 1 mniejsza niż poprzednia if (l > 1) liczba = obliczSilnie1(liczba, l - 1); // liedy l == 1 koniec wywoływania rekurencyjnego metody, // zwracana jest obliczona wartośc return liczba; } } static int obliczSilnie2(int liczba) { // drukowanie przekazanej wartości liczby System.out.println(" Argument: " + liczba); // dla 0 silnia = 1 if (liczba == 0) { return 1; } else { // jeśli liczba jest > 0, zmniejszana jest o 1 i metoda wywołuje sąmą siebie // kiedy metoda zwróci wynik obliczany jest bieżący wynik i jest zwracany "wyżej" int l = obliczSilnie2(liczba - 1); int wynik = liczba * l; // drukowanie obliczonej wartości liczby, która będzie zwrócona System.out.println(" Zwracana: " + wynik); return wynik; } } /** * silnia liczona za pomocą pętli */ static int obliczSilniePetla(int l) { // jeśli liczba jest równa 0 to zwróć 1 if (l == 0) return 1; // jeśli l nie jest równe 0 int wynik = l; for (int i = l-1; i > 1 ; i--) { wynik = wynik * i; } return wynik; } } |
Ja bym zaproponowała nieco łatwiejszy kod:
static int Silnia1 (int liczba)
{
if (liczba==0) return 1;
return liczba*Silnia1(liczba-1);
}
wydaje mi się być równie poprawny
Jasne.
W zasadzie to skondensowana forma obliczSilnie2(). :-)
Pozdrawiam
czesc, a gdzie w obliczSilnie1 jest sprawdzenie czy l =< 1
?
Kiedy wywołuje się metodę:
obliczSilnie1(liczba, l – 1)
przy wartości l=1, to wtedy w wywołanej metodzie liczba == 0, zatem
if (liczba == 0) jest spełnione i zwracana jest jedynka.
Oczywiście można by też wpisać warunek:
if (l != 1) obliczSilnie1(liczba, l – 1);
else return 1;
Nieco dłuższy kod za to jedno niepotrzebne wywołanie metody mniej.
Pozdrawiam
Grzegorz
nie moge sie zgodzic, moim zdaniem
wartosc „liczba” jest zawsze wieksza niz wartosc „l”
w wywolaniu obliczSilnie1(liczba, l – 1);
Słusznie! Żle odczytałem własny kod ;-)
Naniosę poprawki, dzięki za zwrócenie uwagi.
Pozdrawiam
: ) A material sam w sobie bardzo przydatny, Pozdrawiam!
Dziękuję, a kod już poprawiony.
W razie znalezienia kolejnych „bugów” proszę śmiało pisać. :-)