Eksempel på rekursiv kode uten utgangsbetingelse

La oss ta en ny titt på et rekursivt problem. Som et eksempel kan du vurdere å beregne Fibonacci-tall. Alle vil huske at Fibonacci-sekvensen er en numerisk sekvens der de to første tallene er 0 og 1, og hvert påfølgende tall er lik summen av de to foregående tallene.

La oss skrive kode for å beregne og vise disse tallene:

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

Før det første kallet av den rekursive printFibonacci -metoden, skriv ut de to første tallene i sekvensen: null og én. Vi må gjøre dette fordi den rekursive metoden viser bare summen av inngangsparametrene, ikke selve parameterne.

Koden ser ok ut: vi får to tall, beregner summen deres, skriver den ut på konsollen og kaller printFibonacci -metoden rekursivt igjen. Vi sender det forrige tallet (forrige) og det nåværende tallet (nåværende) som argumenter.

Faktisk har koden to feil. Du kan se dem hvis du kjører koden.

Den første feilen flyter over den lange typen. Det 104. tallet i sekvensen vår er negativt, noe som indikerer at den lange typen fløt over.

Den andre feilen er annerledes. Etter å ha beregnet omtrent 12 000 tall, får vi:

Unntak i tråden "hoved" java.lang.StackOverflowError

Nå er et passende tidspunkt å huske hva en metodekallstack er i Java. Java-maskinen holder oversikt over alle funksjonsanrop. For å gjøre dette bruker den en spesiell type samling kalt en stack. Når en funksjon kaller en annen, skyver Java-maskinen et nytt StackTraceElement på stabelen. Når funksjonen avsluttes, fjernes dette elementet fra stabelen. Følgelig lagrer stabelen alltid oppdatert informasjon om den nåværende tilstanden til funksjonsanropsstakken. Dokumentasjonen for StackTraceElement sier at det "kastes når et stackoverflyt oppstår fordi en applikasjon kommer tilbake for dypt." En kjørende JVM har et spesielt minneområde for lagring av metodekallstabelen. Størrelsen på dette minneområdet avhenger av OS- og JVM-innstillingene. I tillegg til selve metodekallstabelen, primitive variabler (spesifikke verdier av metodeparametere) og adressene til referansevariabler (i et minneområde kalt "heapen") er lagret i dette spesielle minneområdet. Tilgang til anropsstakken følger LIFO-prinsippet.

Korrigert eksempel med en utgangstilstand

La oss starte med å fikse det andre problemet i koden.

La oss prøve å løse problemet direkte: hvis stabelen er for liten, så la oss gjøre den større. For å gjøre dette, start JVM med "-Xss"-flagget og spesifiser hvor mye minne som skal allokeres til stabelen. La oss prøve å tildele 5 megabyte. Slik vil det se ut i IDEA:

Vi klarte å øke lengden på utgangen. Nå kan beregne mer enn 49 000 tall av sekvensen i stedet for å være begrenset til omtrent 12 000 tall. Men på et tidspunkt får vi fortsatt en StackOverflowError .

Du kan prøve å øke størrelsen på stabelen, men det løser ikke det grunnleggende problemet. Så la oss se etter et problem i logikken. Det må være et punkt når rekursjonen stopper. Med andre ord må det være en tilstand som bestemmer når den rekursive metoden ikke lenger vil bli kalt, slik at anropsstakken kan slappe av. For å definere en slik betingelse, la oss gjøre målet vårt eksplisitt: vise Fibonacci-serien så lenge tallene er mindre enn heltall.MAX_VALUE .

La oss skrive en ny printFibonacciWithCondition -metode som vil ta hensyn til denne tilstanden. Og vi vil kalle den nye korrigerte metoden i hovedmetoden.

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

Etter å ha kjørt koden ser vi at utdata slutter med tallet 1836311903. Tallet før denne er 1134903170. Summen av disse tallene er 2_971_215_073, som faktisk er større enn Integer.MAX_VALUE (2_147_483_647 ) .

Denne endringen løste automatisk den lange overløpsfeilen. Hvis du trenger å beregne mer av serien, må du bruke andre datatyper, for eksempel BigInteger .

Rekursiv nedstigning og avkobling

La oss analysere trinn for trinn hvordan koden vår utføres. For å gjøre dette, legger vi til en ekkometode og kaller den før og etter det rekursive kallet til printFibonacciWithCondition -metoden.

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

Programmet gir oss denne utgangen:

0
1
Før metodekall med args: 0, 1. Nåværende tall = 1
Før metodekall med args: 1, 1. Nåværende nummer = 2
Før metodekall med args: 1, 2. Nåværende nummer = 3
Før metodekall med args: 2, 3. Nåværende tall = 5
Før metodekall med args: 3, 5. Nåværende tall = 8
Før metodekall med args: 5, 8. Nåværende tall = 13
Før metodekall med args: 8, 13. Nåværende tall = 21
Før metodekall med args: 13, 21. Nåværende tall = 34
Før metodekall med args: 21, 34. Nåværende tall = 55
Før metodekall med args: 34, 55. Nåværende tall = 89
Før metodekall med args: 55, 89. Nåværende tall = 144
Før metodekall med args: 89, 144. Nåværende tall = 233
Før metodekall med args: 144, 233. Nåværende tall = 377
Før metodekall med args: 233, 377. Nåværende tall = 610
Før metodekall med args: 377, 610. Nåværende nummer = 987
Før metodekall med args: 610, 987. Nåværende nummer = 1597
Før metodeanrop med args: 987, 1597. Nåværende nummer = 2584
Før metodekall med args: 1597, 2584. Nåværende nummer = 4181
Før metode anrop med args: 2584, 4181. Nåværende nummer = 6765
Før metodeanrop med args: 4181, 6765. Nåværende nummer = 10946
Før metodeanrop med args: 6765, 10946. Nåværende nummer = 17711
Før metodeanrop med args: 109416, 179416. Nåværende nummer = 28657
Før metodeanrop med args: 17711, 28657. Nåværende nummer = 46368
Før metodeanrop med args: 28657, 46368. Nåværende nummer = 75025
Før metodeanrop med args: 46368, 75025. Nåværende nummer = 12132 args 5,
før metodeanrop med args: 46368, 75025. 121393. Nåværende nummer = 196418
Før metodekall med args: 121393, 196418. Nåværende nummer = 317811
Før metodeanrop med args: 196418, 317811. Nåværende nummer = 514229
Før metodeanrop med args: 2917 før = 2917 nummer: 2917
før = 196418, 317811. metode ring med args: 514229, 832040. Nåværende nummer = 1346269
Før metodeanrop med args: 832040, 1346269. Nåværende nummer = 2178309
Før metodeanrop med args: 1346269, 2178309. Nåværende nummer = 35245
Før metodeanrop med args: 2178309, 3524578. Nåværende nummer = 5702887
Før metodeanrop med args: 3524578, 5702887. Nåværende nummer = 9227465 Før metodeanrop med args: 5702887, 4 Nåværende metodeanrop 5. 922746 args 5. 27465
,
14930352. Nåværende nummer = 24157817
Før metodeanrop med args: 14930352, 24157817. Nåværende nummer = 39088169
Før metodeanrop med args: 24157817, 39088169. Nåværende metodeanrop 5 med 86:6:9
før 86:9 3245986. Nåværende nummer = 102334155
Før metode ring med args: 63245986, 102334155. Nåværende nummer = 165580141
Før metodeanrop med args: 102334155, 165580141. Nåværende nummer = 267914296
Før metodeanrop med args: 165580141, 267914296. Nåværende nummer = 433494437
Før metodeanrop med args: 267914296, 433494437. Nåværende nummer = 701408733 Før metodeanrop med args: 4313,7 nåværende nummer = 4313,7. 34903170
Før
metodeanrop med args: 701408733, 1134903170. Nåværende nummer = 1836311903
Etter metodeanrop med args: 701408733, 113490317
Etter metodeanrop med args: 433494437, 701408733 Etter metodekall med args: 3967903170,
113490317
580141, 267914296
Etter metodekall med args: 102334155, 165580141
Etter metodekall med args: 63245986, 102334155
Etter metodekall med args: 39088169, 63245986
Etter metodeanrop med args: 24157817, 39088169
Etter metodekall med args: 14930352, 24157817
Etter metodeanrop med args: 9227465, 14930352 Etter metodeanrop med args: 5702887, 5702887
,
5727 metoden med args: 572,727 887
Etter metodekall med args : 2178309, 3524578
Etter metodeanrop med args: 1346269, 2178309
Etter metodekall med args: 832040, 1346269 Etter metodekall med args: 514229, 832040 Etter metodekall med args: 812040, 1346269 Etter metodekall med args: 514229, 832040
Etter metodekall med 812040 args: 1 18, 317811 Etter metodekall med args: 121393, 196418 Etter metodekall med args: 75025, 121393 Etter metodekall med args: 46368, 75025





Etter metodekall med args: 28657, 46368
Etter metodekall med args: 17711, 28657
Etter metodekall med args: 10946, 17711 Etter
metodekall med args: 6765, 10946
Etter metodekall med args: 4181, metodekall med args: 4181,
metodekall med args: 6765 : 2584, 4181
Etter metodekall med args: 1597, 2584
Etter metodekall med args: 987, 1597
Etter metodekall med args: 610, 987
Etter metodekall med args: 377, 610
Etter metodekall med args: 233, 377
Etter metodekall med args: 144, 233
Etter metodekall med args: 89, 144
Etter metodekall med args: 55, 89
Etter metodekall med args: 34, 55
Etter metodekall med args: 21, 34
Etter metodekall med args: 13, 21
Etter metodekall med args: 8, 13
Etter metodekall med args: 5, 8
Etter metodekall med args: 3, 5
Etter metodekall med args: 2, 3
Etter metodekall med args: : 1, 2
Etter metodekall med args: 1, 1
Etter metodekall med args: 0, 1

Her er en visualisering av hva som skjer.

La oss si det igjen: metoden printFibonacciWithCondition kalles. Den beregner gjeldende tall. Hvis det passer oss, viser vi det og kaller printFibonacciWithCondition -metoden igjen med nye argumenter.

Så lenge den rekursive metoden kalles, kalles dette "rekursiv nedstigning". Når rekursivt avsluttes og metodeanrop kommer tilbake, sier vi at anropsstakken "avvikles".

Rekursjon er et interessant tema innen programmering. For å få bedre grep om materialet, la oss endre oppgaven litt. Den nye oppgaven er å skrive ut, i synkende rekkefølge, alle tall i Fibonacci-serien som ikke overskrider Integer.MAX_VALUE . Vi har allerede skrevet all koden som trengs for denne oppgaven. Alt som gjenstår er å bytte rekkefølge for å vise gjeldende nummer og ringe den rekursive metoden. Det vil si at i det første eksemplet ble det beregnede tallet vist under "nedstigningen", men nå må vi "ned til bunnen" og deretter vise tall "på vei opp igjen". Og selvfølgelig, i hovedmetoden , bytter vi de to innledende tallene i sekvensen (null og en) etter å ha vist dem etter at vi kaller den rekursive metoden. For lesbarhet,metode.

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

Utgangen vil være:

1836311903
1134903170
701408733 433494437 267914296 165580141 102334155. 87 3524578
2178309 1346269 832040 514229 317811 196418
121393 75025 46368 28657 17711 10946 4 676 7 9 233 144 89 55 34 21 13 8 5 3 2 _ _ _ _ _ _ _ _







































1
1
0