Eksempel på rekursiv kode uden en udgangsbetingelse

Lad os se på et rekursivt problem igen. Som et eksempel kan du overveje at beregne Fibonacci-tal. Alle vil huske, at Fibonacci-sekvensen er en numerisk sekvens, hvor de første to tal er 0 og 1, og hvert efterfølgende tal er lig med summen af ​​de to foregående tal.

Lad os skrive kode for at beregne og vise disse tal:


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

Inden det første kald af den rekursive printFibonacci -metode skal du udskrive de første to tal i sekvensen: nul og et. Vi er nødt til at gøre dette, fordi den rekursive metode kun viser summen af ​​inputparametrene, ikke selve parametrene.

Koden ser ok ud: vi får to tal, beregner deres sum, udskriver den på konsollen og kalder printFibonacci -metoden rekursivt igen. Vi sender det forrige tal (forrige) og det nuværende tal (nuværende) som argumenter.

Faktisk har koden to fejl. Du kan se dem, hvis du kører koden.

Den første fejl flyder over den lange type. Det 104. tal i vores rækkefølge er negativt, hvilket indikerer, at den lange type løb over.

Den anden fejl er anderledes. Efter at have beregnet omkring 12.000 tal, får vi:

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

Nu er et passende tidspunkt at huske, hvad en metodekaldsstack er i Java. Java-maskinen registrerer alle funktionsopkald. For at gøre dette bruger den en særlig form for samling kaldet en stak. Når en funktion kalder en anden, skubber Java-maskinen et nyt StackTraceElement ind på stakken. Når funktionen slutter, fjernes dette element fra stakken. Følgelig gemmer stakken altid opdateret information om den aktuelle tilstand af funktionsopkaldsstakken. Dokumentationen til StackTraceElement siger, at det "kastes, når et stackoverløb opstår, fordi en applikation gentager sig for dybt." En kørende JVM har et særligt hukommelsesområde til lagring af metodekaldsstakken. Størrelsen af ​​dette hukommelsesområde afhænger af OS- og JVM-indstillingerne. Ud over selve metodekaldsstakken, primitive variabler (specifikke værdier af metodeparametre) og adresserne på referencevariabler (i et hukommelsesområde kaldet "heapen") er lagret i dette specielle hukommelsesområde. Adgang til opkaldsstakken følger LIFO-princippet.

Korrigeret eksempel med en udgangstilstand

Lad os starte med at løse det andet problem i koden.

Lad os prøve at løse problemet direkte: Hvis stakken er for lille, så lad os gøre den større. For at gøre dette skal du starte JVM med "-Xss" flaget og angive, hvor meget hukommelse der skal allokeres til stakken. Lad os prøve at tildele 5 megabyte. Sådan kommer det til at se ud i IDEA:

Det lykkedes os at øge længden af ​​output. Nu kan beregne mere end 49.000 numre af sekvensen i stedet for at være begrænset til omkring 12.000 numre. Men på et tidspunkt får vi stadig en StackOverflowError .

Du kan prøve at øge størrelsen af ​​stakken, men det løser ikke det grundlæggende problem. Så lad os se efter et problem i logikken. Der må være et tidspunkt, hvor rekursionen stopper. Med andre ord skal der være en betingelse, der bestemmer, hvornår den rekursive metode ikke længere vil blive kaldt, så opkaldsstakken kan afvikles. For at definere en sådan betingelse, lad os gøre vores mål eksplicit: Vis Fibonacci-serien, så længe dens tal er mindre end Integer.MAX_VALUE .

Lad os skrive en ny printFibonacciWithCondition -metode, der vil tage højde for denne betingelse. Og vi vil kalde den nye korrigerede metode 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);
    }
}
    

Efter at have kørt koden, ser vi, at outputtet slutter med tallet 1836311903. Tallet før denne er 1134903170. Summen af ​​disse tal er 2_971_215_073, hvilket faktisk er større end Integer.MAX_VALUE (2_147_483_647 ) .

Denne ændring rettede automatisk den lange overløbsfejl. Hvis du skal beregne mere af serien, skal du bruge andre datatyper, såsom BigInteger .

Rekursiv nedstigning og afslapning

Lad os trin for trin analysere, hvordan vores kode udføres. For at gøre dette tilføjer vi en ekkometode og kalder den før og efter det rekursive kald af 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 giver os dette output:

0
1
Før metodekald med args: 0, 1. Aktuelt tal = 1
Før metodekald med args: 1, 1. Aktuelt tal = 2
Før metodekald med args: 1, 2. Aktuelt tal = 3
Før metodekald med args: 2, 3. Aktuelt tal = 5
Før metodekald med args: 3, 5. Aktuelt tal = 8
Før metodekald med args: 5, 8. Aktuelt tal = 13
Før metodekald med args: 8, 13. Aktuelt tal = 21
Før metodekald med args: 13, 21. Aktuelt tal = 34
Før metodekald med args: 21, 34. Aktuelt tal = 55
Før metodekald med args: 34, 55. Aktuelt tal = 89
Før metodekald med args: 55, 89. Nuværende tal = 144
Før metodekald med args: 89, 144. Aktuelt tal = 233
Før metodekald med args: 144, 233. Aktuelt tal = 377
Før metodekald med args: 233, 377. Aktuelt tal = 610
Før metodekald med args: 377, 610. Nuværende nummer = 987
Før metodekald med args: 610, 987. Aktuelt nummer = 1597
Før metodekald med args: 987, 1597. Nuværende tal = 2584
Før metodekald med args: 1597, 2584. Nuværende nummer = 4181
Før metode opkald med args: 2584, 4181. Aktuelt nummer = 6765
Før metodekald med args: 4181, 6765. Aktuelt nummer = 10946
Før metodekald med args: 6765, 10946. Aktuelt nummer = 17711
Før metodekald med args: 179416, 179416. Nuværende nummer = 28657
Før metodekald med args: 17711, 28657. Aktuelt nummer = 46368
Før metodekald med args: 28657, 46368. Aktuelt nummer = 75025
Før metodekald med args: 46368, 75025. Aktuelt nummer = 121393
med args: Før metodekald 5, 750 121393. Nuværende nummer = 196418
Før metodekald med args: 121393, 196418. Aktuelt nummer = 317811
Før metodekald med args: 196418, 317811. Nuværende nummer = 514229 Før
metodekald med args: 2917, 18881: 2,317 nummer: 2917: 2,317 metode
ring med args: 514229, 832040. Nuværende nummer = 1346269
Før metode opkald med args: 832040, 1346269. Nuværende nummer = 2178309
Før metodekald med args: 1346269, 2178309. Nuværende nummer = 35245
Før metodekald med args: 2178309, 3524578. Aktuelt nummer = 5702887
Før metodekald med args: 3524578, 5702887. Aktuelt nummer = 9227465 Før metodekald med args: 5702887, 4 Nuværende metodekald med args:
922703 med 5. Nuværende metode 5. 27465,
14930352. Aktuelt nummer = 24157817
Før metodekald med args: 14930352, 24157817. Aktuelt nummer = 39088169
Før metodekald med args: 24157817, 39088169. Nuværende metodekald 5 med args, 86:6: 9 3245986.
Nuværende nummer = 102334155
Før metode opkald med args: 63245986, 102334155. Nuværende nummer = 165580141
Før metodeopkald med args: 102334155, 165580141. Nuværende nummer = 267914296
Før metodekald med args: 165580141, 267914296. Aktuelt nummer = 433494437
Før metodekald med args: 267914296, 433494437. Nuværende nummer = 701408733 Før metodekald med args: 4437, 7. Nuværende nummer = 4437,7. 34903170
Før
metodekald med args: 701408733, 1134903170. Nuværende nummer = 1836311903
Efter metodekald med args: 701408733, 113490317
Efter metodekald med args: 433494437, 701408733 Efter metodekald med args: 296790 4419
med args: 3779, 4419: 3
580141, 267914296
Efter metodekald med args: 102334155, 165580141
Efter metodekald med args: 63245986, 102334155
Efter metodekald med args: 39088169, 63245986
Efter metodekald med args: 24157817, 39088169
Efter metodekald med args: 14930352, 24157817
Efter metodekald med args: 9227465, 14930352 Efter metodekald med args: 5702887, 5702887
, 5727
metode kald med 5:727, 5727, 5727 887
Efter metodekald med args : 2178309, 3524578
Efter metodekald med args: 1346269, 2178309
Efter metodekald med args: 832040, 1346269 Efter
metodekald med args: 514229
, 832040 Efter
metodekald med args: 1240, 134629 18, 317811
Efter metodekald med args: 121393, 196418
Efter metodekald med args: 75025, 121393
Efter metodekald med args: 46368, 75025
Efter metodekald med args: 28657, 46368
Efter metodekald med args: 17711, 28657
Efter metodekald med args: 10946, 17711
Efter metodekald med args: 6765, 10946 Efter metodekald med args: 4181, metodekald med args: Efter metodekald med args
: 6765, 10946
: 2584, 4181
Efter metodekald med args: 1597, 2584
Efter metodekald med args: 987, 1597
Efter metodekald med args: 610, 987
Efter metodekald med args: 377, 610
Efter metodekald med args: 233, 377
Efter metodekald med args: 144, 233
Efter metodekald med args: 89, 144
Efter metodekald med args: 55, 89
Efter metodekald med args: 34, 55
Efter metodekald med args: 21, 34
Efter metodekald med args: 13, 21
Efter metodekald med args: 8, 13
Efter metodekald med args: 5, 8
Efter metodekald med args: 3, 5
Efter metodekald med args: 2, 3
Efter metodekald med args: : 1, 2
Efter metodekald med args: 1, 1
Efter metodekald med args: 0, 1

Her er en visualisering af, hvad der sker.

Lad os sige det igen: metoden printFibonacciWithCondition kaldes. Den beregner det aktuelle antal. Hvis det passer os, så viser vi det og kalder printFibonacciWithCondition -metoden igen med nye argumenter.

Så længe den rekursive metode kaldes, kaldes denne "rekursiv afstamning". Når det rekursive afsluttes, og metodekaldene vender tilbage, siger vi, at opkaldsstakken "vinder af".

Rekursion er et interessant emne inden for programmering. For at få bedre styr på materialet, lad os ændre vores opgave lidt. Den nye opgave er at udlæse, i faldende rækkefølge, alle tal i Fibonacci-serien, der ikke overstiger Integer.MAX_VALUE . Vi har allerede skrevet al den nødvendige kode til denne opgave. Det eneste, der er tilbage, er at skifte rækkefølgen for at vise det aktuelle nummer og kalde den rekursive metode. Det vil sige, at i det første eksempel blev det beregnede tal vist under "nedstigningen", men nu skal vi "ned til bunden" og derefter vise tal "på vej op igen". Og selvfølgelig, i hovedmetoden , bytter vi de to indledende tal i sekvensen (nul og en) efter at have vist dem, efter at vi kalder den rekursive metode. For læsbarheden,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);
    }
}
    

Outputtet vil være:

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