Példa rekurzív kódra kilépési feltétel nélkül

Nézzünk még egy pillantást egy rekurzív problémára. Példaként tekintsük a Fibonacci-számok kiszámítását. Mindenki emlékszik rá, hogy a Fibonacci-sorozat egy olyan numerikus sorozat, amelyben az első két szám 0 és 1, és minden további szám egyenlő az előző két szám összegével.

Írjunk kódot a következő számok kiszámításához és megjelenítéséhez:


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);
    }
}
    

A rekurzív printFibonacci metódus első hívása előtt nyomtassa ki a sorozat első két számát: nullát és egyet. Ezt azért kell megtennünk, mert a rekurzív metódus csak a bemeneti paraméterek összegét jeleníti meg, magukat a paramétereket nem.

A kód rendben van: kapunk két számot, kiszámoljuk az összegüket, kinyomtatjuk a konzolra, és újra meghívjuk a printFibonacci metódust rekurzívan. Az előző számot (előző) és az aktuális számot (jelenlegi) adjuk át argumentumként.

Valójában a kódnak két hibája van. Megtekintheti őket, ha futtatja a kódot.

Az első hiba a hosszú típus túlcsordulása . A sorozatunkban a 104. szám negatív, ami azt jelzi, hogy a hosszú típus túlcsordult.

A második hiba más. Nagyjából 12 000 szám kiszámítása után kapjuk:

Kivétel a "main" szálban java.lang.StackOverflowError

Itt az alkalom, hogy felidézzük, mi az a módszerhívási verem a Java-ban. A Java gép minden függvényhívást rögzít. Ehhez egy speciális gyűjteményt használ, amelyet veremnek neveznek. Amikor egy függvény meghív egy másikat, a Java gép egy új StackTraceElement elemet helyez a verembe. Amikor a függvény véget ér, ez az elem eltávolításra kerül a veremből. Ennek megfelelően a verem mindig naprakész információkat tárol a függvényhívási verem aktuális állapotáról. A StackTraceElement dokumentációja azt írja, hogy "eldobják, ha veremtúlcsordulás történik, mert egy alkalmazás túl mélyen ismétlődik." A futó JVM-nek van egy speciális memóriaterülete a metódushívási verem tárolására. A memóriaterület mérete az operációs rendszer és a JVM beállításaitól függ. A metódushívási verem mellett A primitív változók (a metódusparaméterek specifikus értékei) és a referenciaváltozók címei (a "kupacnak" nevezett memóriaterületen) ebben a speciális memóriaterületen tárolódnak. A hívásveremhez való hozzáférés a LIFO elvet követi.

Javított példa egy kilépési feltétellel

Kezdjük a kód második problémájának kijavításával.

Próbáljuk meg megoldani a problémát: ha túl kicsi a verem, akkor növeljük meg. Ehhez indítsa el a JVM-et az "-Xss" jelzővel, és adja meg, hogy mennyi memóriát kell lefoglalni a verem számára. Próbáljunk 5 megabájtot lefoglalni. Így fog kinézni az IDEA-ban:

Sikerült megnövelni a kimenet hosszát. Most már több mint 49 000 számot tud kiszámolni a sorozatból, ahelyett, hogy körülbelül 12 000 számra korlátozódna. De egy ponton továbbra is StackOverflowError üzenetet kapunk .

Megpróbálhatja növelni a verem méretét, de ez nem oldja meg az alapvető problémát. Tehát keressünk egy problémát a logikában. Kell lennie egy pontnak, amikor a rekurzió leáll. Más szavakkal, léteznie kell valamilyen feltételnek, amely meghatározza, hogy a rekurzív metódus mikor nem hívódik többé, lehetővé téve a hívási verem feloldását. Egy ilyen feltétel meghatározásához tegyük explicitté a célunkat: jelenítsük meg a Fibonacci-sort, amíg a száma kisebb, mint Integer.MAX_VALUE .

Írjunk egy új printFibonacciWithCondition metódust, amely figyelembe veszi ezt a feltételt. Az új korrigált metódust pedig a főmetódusban fogjuk meghívni.


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);
    }
}
    

A kód futtatása után azt látjuk, hogy a kimenet a 1836311903 számmal végződik. Az előtte lévő szám 1134903170. Ezeknek a számoknak az összege 2_971_215_073, ami valóban nagyobb, mint Integer.MAX_VALUE (2_147_483_647 ) .

Ez a változtatás automatikusan kijavította a hosszú túlcsordulási hibát. Ha többet kell kiszámítania a sorozatból, akkor más adattípusokat kell használnia, például a BigIntegert .

Rekurzív leereszkedés és feloldás

Lépésről lépésre elemezzük a kódunk végrehajtását. Ehhez hozzáadunk egy echo metódust, és meghívjuk a printFibonacciWithCondition metódus rekurzív hívása előtt és után.


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);
        }
    }
}
    

A program ezt a kimenetet adja:

0
1
Metódushívás előtt arg-okkal: 0, 1. Aktuális szám = 1
Metódushívás előtt argokkal: 1, 1. Aktuális szám = 2
Metódushívás előtt arg-okkal: 1, 2. Aktuális szám = 3
Metódushívás előtt arg-okkal: 2, 3. Aktuális szám = 5
Metódushívás előtt argokkal: 3, 5. Aktuális szám = 8
Metódushívás előtt argokkal: 5, 8. Jelenlegi szám = 13
Metódushívás előtt argokkal: 8, 13. Jelenlegi szám = 21
Metódushívás előtt argokkal: 13, 21. Aktuális szám = 34
Metódushívás előtt argokkal: 21, 34. Aktuális szám = 55
Metódushívás előtt argokkal: 34, 55. Aktuális szám = 89
Metódushívás előtt argokkal: 55, 89. Jelenlegi szám = 144
Metódushívás előtt argokkal: 89, 144. Aktuális szám = 233
Metódushívás előtt argokkal: 144, 233. Aktuális szám = 377
Metódushívás előtt argokkal: 233, 377. Aktuális szám = 610
Metódushívás előtt argokkal: 377, 610. Aktuális szám = 987
Metódushívás előtt argokkal: 610, 987. Aktuális szám = 1597
Metódushívás előtt argokkal: 987, 1597. Jelenlegi szám = 2584
Metódushívás előtt argokkal: 1597, 2584. Aktuális szám = 4181
Metódus előtt hívás argokkal: 2584, 4181. Aktuális szám = 6765
Metódushívás előtt argokkal: 4181, 6765. Jelenlegi szám = 10946
Metódushívás előtt argokkal: 6765, 10946. Aktuális szám = 17711
Metódushívás előtt argokkal: 109741. Jelenlegi szám = 28657
Metódushívás előtt arg-okkal: 17711, 28657. Aktuális szám = 46368
Metódushívás előtt argokkal: 28657, 46368. Jelenlegi szám = 75025
Metódushívás előtt arg-okkal: 46368, 75025. Aktuális szám = 121393, 7-es
metódus hívása 0 ar2-tel 121393. Aktuális szám = 196418
Metódushívás előtt argokkal: 121393, 196418. Jelenlegi szám = 317811
Metódushívás előtt argokkal: 196418, 317811. Aktuális szám = 514229
Metódushívás előtt argokkal: 31308181 előtt: 3157481
előtt. módszerrel hívás argokkal: 514229, 832040. Jelenlegi szám = 1346269
Metódushívás előtt argokkal: 832040, 1346269. Jelenlegi szám = 2178309
Metódushívás előtt argokkal: 1346269, 2178309. Aktuális szám: 2178309.
Metódushívás előtt arg-okkal: 2178309, 3524578. Aktuális szám = 5702887
Metódushívás előtt argokkal: 3524578, 5702887. Aktuális szám = 9227465 Metódushívás előtt arg-okkal: 5702887
, 922746 Előtt metódus hívása argokkal: 5702887, 922746
Előtt 3-as metódussal: 5 args:9:2. 27465, 14930352. Aktuális szám = 24157817
Metódushívás előtt arg-ekkel: 14930352, 24157817. Aktuális szám = 39088169 Metódushívás
előtt arg-ekkel: 24157817, 39088169. Aktuális szám args-el, 39088169. Módszerhívás előtt
: 8, 6 6:39 3245986. Jelenlegi szám = 102334155
Módszer előtt hívás argokkal: 63245986, 102334155. Jelenlegi szám = 165580141
Módszerhívás előtt args: 102334155, 165580141. Jelenlegi szám = 267914296
Metódushívás előtt argokkal: 165580141, 267914296. Aktuális szám = 433494437
Metódushívás előtt arg-okkal: 267914296, 433494437. Aktuális szám = 701408733 Metódushívás előtt arg-okkal: 437,494, aktuális szám: 437,4943. 34903170
Metódushívás
előtt arg-okkal: 701408733, 1134903170. Aktuális szám = 1836311903
Metódushívás után arg-ekkel: 701408733, 113490317
Metódushívás után arg-ekkel: 433494437, 701408733 Metódushívás után arg-okkal: 2679147
metódus hívása után args-el: 2679147 args-vel
: 26791447 580141, 267914296
metódushívás után arg-ekkel: 102334155, 165580141
Metódushívás után argokkal: 63245986, 102334155
Metódushívás után arg-ekkel: 39088169, 63245986
Metódushívás után arg-ekkel: 24157817, 39088169
Metódushívás után args-el: 14930352, 24157817
Metódushívás után args-el: 9227465, 14930352 Metódushívás után args-el: 5702887, 6 után 5:7-es metódussal : 5702887,
6 után 9227,23
887
Metódushívás után arg-okkal : 2178309, 3524578
Metódushívás után arg-ekkel: 1346269, 2178309
Metódushívás után args-el: 832040, 1346269 Metódushívás
után args-el: 514229, 832040 Metódushívás után 1-es metódushívás után args-sel: 1, 4:5 18
,
317811
Utána metódushívás arg-okkal: 121393, 196418
Metódushívás után arg-ekkel: 75025, 121393
Metódushívás után arg-ekkel: 46368, 75025
Metódushívás után arg-ekkel: 28657, 46368
Metódushívás után arg-ekkel: 17711, 28657
Metódushívás után args-sel: 10946
, 17711 Metódushívás után args-sel: 6765, 10946 Metódushívás után
args-el: 4181,
metódushívás után args-el : 2584, 4181
Metódushívás után argokkal: 1597, 2584
Metódushívás után arg-ekkel: 987, 1597
Metódushívás után arg-ekkel: 610, 987
Metódushívás után arg-ekkel: 377, 610
Metódushívás után arg-ekkel: 233, 377
után metódushívás arg-okkal: 144, 233
metódushívás után arg-okkal: 89, 144
metódushívás után arg-okkal: 55, 89
metódushívás után arg-okkal: 34, 55 Metódushívás
után arg-okkal: 21, 34
Metódushívás után arg-okkal: 13, 21
Metódushívás után arg-okkal: 8, 13
Metódushívás után arg-okkal: 5, 8
Metódushívás után arg-ekkel: 3, 5 Metódushívás
után args-ekkel: 2, 3 Metódushívás
után args-ekkel : 1, 2
Metódushívás után argokkal: 1, 1
Metódushívás után argokkal: 0, 1

Íme egy vizualizáció, hogy mi történik.

Mondjuk el még egyszer: a printFibonacciWithCondition metódust hívják. Kiszámolja az aktuális számot. Ha megfelel nekünk, akkor megjelenítjük, és új argumentumokkal újra meghívjuk a printFibonacciWithCondition metódust.

Amíg a rekurzív metódus hívva van, addig ezt "rekurzív leszállásnak" nevezik. Amikor a rekurzív befejeződik, és a metódushívások visszatérnek, azt mondjuk, hogy a hívásverem „letekerődik”.

A rekurzió érdekes téma a programozásban. Ahhoz, hogy jobban kezeljük az anyagot, változtassunk kissé a feladatunkon. Az új feladat az, hogy csökkenő sorrendben kiadja a Fibonacci sorozat összes olyan számát, amely nem haladja meg az Integer.MAX_VALUE értéket . Már megírtuk az ehhez a feladathoz szükséges összes kódot. Már csak az aktuális szám megjelenítési sorrendjét és a rekurzív metódus hívását kell felcserélni. Ez azt jelenti, hogy az első példában a kiszámított szám a „leereszkedés” során jelent meg, most viszont „le kell ereszkednünk” és utána „visszafelé menet” ki kell mutatnunk a számokat. És természetesen a metódusban felcseréljük a sorozat két kezdőszámát (nulla és egy), miután megjelenítettük őket, miután meghívtuk a rekurzív metódust. Az olvashatóság érdekébenmódszer.


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);
    }
}
    

A kimenet a következő lesz:

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