Przykład kodu rekurencji bez warunku wyjścia

Przyjrzyjmy się jeszcze raz problemowi rekurencyjnemu. Jako przykład rozważmy wyszukiwanie liczb Fibonacciego. Kto nie pamięta, ciąg Fibonacciego to elementy ciągu liczbowego, w którym pierwsze dwie liczby to 0 i 1, a każda kolejna liczba jest równa sumie dwóch poprzednich liczb.

Napiszmy kod, aby znaleźć i wyświetlić takie liczby:


public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
        printFibonacci(0, 1);
    }

    private static void printFibonacci(long penultimate, long previous) {
        long current = penultimate + previous;
        System.out.println(current);
        printFibonacci(previous, current);
    }
}
    

Przed pierwszym wywołaniem metody rekurencyjnej printFibonacci wypisz dwie pierwsze liczby z sekwencji, zero i jedynkę. Jest to konieczne, ponieważ w metodzie rekurencyjnej wyświetlamy tylko sumę otrzymanych parametrów, a nie same parametry.

Wygląda OK: otrzymujemy dwie liczby, obliczamy ich sumę, wypisujemy na konsolę i ponownie wywołujemy metodę rekurencyjną printFibonacci . Jako parametry przekazujemy numer poprzedni (poprzedni) i numer bieżący (bieżący).

Właściwie w tym kodzie są 2 błędy. Możesz je zobaczyć, jeśli uruchomisz kod.

Pierwszym błędem jest przepełnienie typu long . 104. liczba w naszym ciągu jest już ujemna, co oznacza, że ​​nastąpiło długie przepełnienie .

Drugi błąd jest inny. Po znalezieniu 12 tysięcy kopiejek na ekranie pojawia się:

Wyjątek w wątku „main” java.lang.StackOverflowError

W tym miejscu należy przypomnieć, czym jest stos wywołań metod w Javie. Maszyna Java przechowuje zapis wszystkich wywołań funkcji. Ma do tego specjalną kolekcję - stos (stos). Kiedy jedna funkcja wywołuje inną, maszyna Java umieszcza nowy element StackTraceElement na stosie. Po zakończeniu funkcji element ten jest usuwany ze stosu. Stos ten przechowuje więc zawsze aktualne informacje o aktualnym stanie „stosu wywołań funkcji”. Dosłowne tłumaczenie StackOverflowError– „stos pełny”. Javadoc mówi „Zgłaszany, gdy stos wywołań jest zbyt głęboki”. Działająca JVM ma specjalny obszar pamięci do przechowywania stosu wywołań metod. Rozmiar tego obszaru pamięci zależy od ustawień systemu operacyjnego i JVM. Oprócz samego stosu wywołań metod, w tym obszarze pamięci przechowywane są zmienne pierwotne (określone wartości parametrów wywołań metod) oraz adresy zmiennych referencyjnych (w obszarze pamięci HEAP). Model dostępu do stosu to LIFO.

Poprawiony przykład warunku wyjścia

Zacznijmy naprawiać nasz kod od drugiego problemu.

Spróbujmy rozwiązać problem bezpośrednio: jeśli rozmiar stosu jest mały, zwiększmy go. Aby to zrobić, musisz uruchomić JVM ze specjalną flagą „-Xss” i określić, ile pamięci przydzielić dla stosu. Spróbujmy przydzielić 5 megabajtów. W IDEA będzie wyglądać tak:

Tak, długość wyjściowa wzrosła i teraz nie jest to ponad 12 tysięcy znalezionych liczb sekwencji, ale ponad 49 tysięcy. Ale po pewnym numerze nadal otrzymujemy StackOverflowError .

Możesz spróbować zwiększyć obszar pamięci stosu, ale zasadniczo nic się nie zmienia. Będziemy więc szukać problemu w logice. Rekurencja musi mieć punkt przerwania. Oznacza to, że musi istnieć jakiś warunek, po spełnieniu którego metoda rekurencyjna nie zostanie wywołana, a stos wywołań zostanie zwrócony. Aby zdefiniować taki warunek, określmy zadanie - wyświetlić szeregi liczb Fibonacciego, o ile są one mniejsze niż Integer.MAX_VALUE .

Napiszmy nową metodę printFibonacciWithCondition , w której uwzględnimy ten warunek. A w głównej metodzie wywołamy nową poprawioną metodę.


public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
//        printFibonacci(0, 1);
        printFibonacciWithCondition(0, 1);
    }

    private static void printFibonacci(long penultimate, long previous) {
        long current = penultimate + previous;
        System.out.println(current);
        printFibonacci(previous, current);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        System.out.println(current);
        printFibonacciWithCondition(previous, current);
    }
}
    

Po uruchomieniu kodu naprawdę widzimy, że wyjście zakończyło się liczbą 1836311903. Wcześniej ta liczba wynosiła 1134903170. Ich suma to 2_971_215_073, czyli naprawdę więcej niż Integer.MAX_VALUE (2_147_483_647) .

Wraz z tą poprawką automatycznie naprawiliśmy błąd długiego przepełnienia . Jeśli potrzebujesz dłuższej serii liczb, musisz użyć innych typów danych, takich jak BigInteger .

Rekurencyjna metoda zejścia i powrotu

Spróbujmy krok po kroku przeanalizować sposób wykonania naszego kodu. Aby to zrobić, dodaj metodę echo i wywołaj ją przed i po rekurencyjnym wywołaniu metody printFibonacciWithCondition .


public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
        printFibonacciWithCondition(0, 1);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        echo(true, penultimate, previous);
        System.out.println(current);
        printFibonacciWithCondition(previous, current);
        echo(false, penultimate, previous);
    }

    private static void echo(boolean isBeforeRecursiveCall, long penultimate, long previous) {
        if (isBeforeRecursiveCall) {
            System.out.printf("Before method call with args: %d, %d. Current number = ", penultimate, previous);
        } else {
            System.out.printf("After method call with args: %d, %d\n", penultimate, previous);
        }
    }
}
    

W wyniku działania programu otrzymujemy wynik:

0
1
Przed wywołaniem metody z argumentami: 0, 1. Bieżąca liczba = 1
Przed wywołaniem metody z argumentami: 1, 1. Bieżąca liczba = 2
Przed wywołaniem metody z argumentami: 1, 2. Bieżąca liczba = 3
Przed wywołaniem metody z argumentami: 2, 3. Bieżąca liczba = 5
Przed wywołaniem metody z argumentami: 3, 5. Bieżąca liczba = 8
Przed wywołaniem metody z argumentami: 5, 8. Bieżąca liczba = 13
Przed wywołaniem metody z argumentami: 8, 13. Bieżąca liczba = 21
Przed wywołaniem metody z argumentami: 13, 21. Bieżąca liczba = 34
Przed wywołaniem metody z argumentami: 21, 34. Bieżąca liczba = 55
Przed wywołaniem metody z argumentami: 34, 55. Bieżąca liczba = 89
Przed wywołaniem metody z argumentami: 55, 89. Aktualny numer = 144
Przed wywołaniem metody z argumentami: 89, 144. Aktualny numer = 233
Przed wywołaniem metody z argumentami: 144, 233. Aktualny numer = 377
Przed wywołaniem metody z argumentami: 233, 377. Aktualny numer = 610
Przed wywołaniem metody z argumentami: 377, 610. Bieżący numer = 987
Przed wywołaniem metody z argumentami: 610, 987. Bieżący numer = 1597
Przed wywołaniem metody z argumentami: 987, 1597. Bieżący numer = 2584
Przed wywołaniem metody z argumentami: 1597, 2584. Bieżący numer = 4181
Przed metodą wywołanie z argumentami: 2584, 4181. Aktualny numer = 6765
Przed wywołaniem metody z argumentami: 4181, 6765. Aktualny numer = 10946
Przed
wywołaniem metody z argumentami: 6765, 10946. Aktualny numer = 17711 bieżący numer = 28657
Przed wywołaniem metody z argumentami: 17711, 28657. Aktualny numer = 46368
Przed wywołaniem metody z argumentami: 28657, 46368. Aktualny numer = 75025. Przed wywołaniem metody z argumentami
: 46368, 75025. Aktualny numer Przed wywołaniem metody z argumentami: 121393, 196418. Bieżący numer = 317811 Przed wywołaniem metody z argumentami: 196418, 317811. Bieżący numer = 514 229 Przed wywołaniem metody z argumentami: 317811, 514229. Bieżący numer = 832040 Przed wywołaniem metody z argumentami: 514229 , 832040. Bieżący numer = 1346269. Przed wywołaniem metody z argumentami: 832040 , 1346269. Bieżący numer = 2178309.







Przed wywołaniem metody z argumentami: 2178309, 3524578 Aktualny numer = 5702887
Przed wywołaniem metody z argumentami: 3524578, 5702887 Aktualny numer = 9227465 Przed wywołaniem metody z argumentami: 5702887, 9227465. Aktualny numer = 14930352 227465, 149 30352
Bieżący
numer = 24157817
Przed wywołaniem metody z argumentami: 14930352
, 24157817
3245986. Aktualny numer = 102334155
Przed wywołaniem metody z argumentami: 63245986, 102334155. Aktualny numer = 165580141
Przed wywołaniem metody z argumentami: 102334155, 165580141. Aktualny numer = 267914296
Przed wywołaniem metody z argumentami: 165580141, 267914296. Bieżący numer = 433494437
Przed wywołaniem metody z argumentami: 267914296, 433494437. Bieżący numer = 701408733 34903170 Przed wywołaniem metody
z
argumentami: 701408733, 1134903 170. Aktualny numer = 1836311903
Po wywołaniu metody z argumentami: 701408733 , 113490317 Po wywołaniu metody z argumentami: 433494437
, 701408733 165580141 , 267914296 Po wywołaniu metody z argumentami: 102334155, 165580141 Po wywołaniu metody z argumentami: 63245986, 102334155 Po wywołanie metody z argumentami: 39088169, 63245986





Po wywołaniu metody z argumentami: 24157817, 39088169
Po wywołaniu metody z argumentami: 14930352, 24157817 Po wywołaniu metody
z argumentami: 9227465, 14930352 5702887 Po wywołaniu metody z argumentami: 2178309, 3524578 Po wywołaniu metody z argumentami: 1346269, 2178309 Po wywołaniu metody with args: 832040 , 1346269 Po wywołaniu metody z argumentami: 514229 , 832040 196418, 317811 Po wywołaniu metody z argumentami: 121393, 196418 Po wywołaniu metody z argumentami: 75025, 121393 Po wywołaniu metody z argumentami: 46368, 7 5025











Po wywołaniu metody z argumentami: 28657, 46368
Po wywołaniu metody z argumentami: 17711, 28657
Po wywołaniu metody z
argumentami: 10946, 17711 Po wywołaniu metody z argumentami
: 6765, 10946 Po wywołaniu metody z argumentami: 1597, 2584 Po wywołanie metody z argumentami: 987 Po wywołaniu metody z argumentami: 610, 987 , 1597 144 Po wywołaniu metody z argumentami: 55, 89 Po wywołaniu metody z argumentami: 34 , 55 Po wywołaniu metody z argumentami: 21, 34











Po wywołaniu metody z argumentami: 13, 21
Po wywołaniu metody z argumentami: 8, 13
Po wywołaniu metody z argumentami: 5, 8
Po wywołaniu metody z argumentami: 3, 5
Po wywołaniu metody z argumentami: 2, 3
Po wywołaniu metody z argumentami: : 1, 2
Po wywołaniu metody z argumentami: 1, 1
Po wywołaniu metody z argumentami: 0, 1

Zilustrujmy graficznie, jak to się dzieje.

Powiedzmy to jeszcze raz: metoda printFibonacciWithCondition nazywa się . Oblicza aktualną liczbę. Jeśli nam to odpowiada, to ją wyświetlamy i ponownie wywołujemy metodę printFibonacciWithCondition z nowymi parametrami.

Podczas wywoływania metody rekurencyjnej nazywa się to „zejściem rekurencyjnym”. Gdy na stosie wywołań występuje zwrot - „Zwrot rekurencyjny”.

Rekurencja to ciekawy temat w programowaniu. Dla lepszego przyswojenia materiału zmienimy nieco nasze zadanie. Musisz wypisać serię liczb Fibonacciego nieprzekraczających Integer.MAX_VALUE w porządku malejącym. Aby rozwiązać ten problem, napisaliśmy już cały kod. Pozostaje tylko zamienić wyjście bieżącej liczby i wywołać metodę rekurencyjną. Oznacza to, że w pierwszym przykładzie wyjście znalezionej liczby nastąpiło „na etapie zejścia”, a teraz musimy „zejść na sam dół” i wyświetlić liczby na etapie „powrotu”. I oczywiście w metodzie main wyjście dwóch początkowych liczb sekwencji (zero i jedynka) jest zamieniane i wyprowadzane po wywołaniu metody rekurencyjnej. Dla czytelności usuniemy metodę echo .


public class Fibonacci {
    public static void main(String[] args) {
        printFibonacciWithCondition(0, 1);
        System.out.println(1);
        System.out.println(0);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        printFibonacciWithCondition(previous, current);
        System.out.println(current);
    }
}
    

Wyjście będzie:

1836311903
1134903170
701408733
433494437
267914296
165580141
102334155
63245986
39088169
24157817
14930352
9227465
5702887
3524578
2178309
1346269
832040
514229
317811
196418
121393
75025
46368
28657
17711
10946
6765
4181
2584
1597
987
610
377
233
144
89
55
34
21
13
8
5
3
2
1
1
0