Exempel på rekursiv kod utan utgångsvillkor

Låt oss ta en ny titt på ett rekursivt problem. Som ett exempel, överväg att beräkna Fibonacci-tal. Alla kommer ihåg att Fibonacci-sekvensen är en numerisk sekvens där de två första talen är 0 och 1, och varje efterföljande tal är lika med summan av de två föregående talen.

Låt oss skriva kod för att beräkna och visa dessa siffror:


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öre det första anropet av den rekursiva printFibonacci- metoden, skriv ut de två första siffrorna i sekvensen: noll och ett. Vi måste göra detta eftersom den rekursiva metoden endast visar summan av ingångsparametrarna, inte själva parametrarna.

Koden ser okej ut: vi får två siffror, beräknar deras summa, skriver ut den på konsolen och anropar printFibonacci -metoden rekursivt igen. Vi skickar det föregående numret (föregående) och det nuvarande numret (nuvarande) som argument.

Faktum är att koden har två fel. Du kan se dem om du kör koden.

Det första felet svämmar över den långa typen. Det 104:e talet i vår sekvens är negativt, vilket indikerar att den långa typen svämmade över.

Det andra felet är annorlunda. Efter att ha beräknat ungefär 12 000 tal får vi:

Undantag i tråden "huvud" java.lang.StackOverflowError

Nu är en lämplig tidpunkt att komma ihåg vad en metodanropsstack är i Java. Java-maskinen registrerar alla funktionsanrop. För att göra detta använder den en speciell sorts samling som kallas en stack. När en funktion anropar en annan, skjuter Java-maskinen ett nytt StackTraceElement till stacken. När funktionen avslutas tas detta element bort från stacken. Följaktligen lagrar stacken alltid aktuell information om det aktuella tillståndet för funktionsanropsstacken. Dokumentationen för StackTraceElement säger att det är "Kasta när ett stackspill inträffar eftersom en applikation återkommer för djupt." En körande JVM har ett speciellt minnesområde för lagring av metodanropsstacken. Storleken på detta minnesområde beror på OS- och JVM-inställningarna. Förutom själva metodanropsstacken, primitiva variabler (specifika värden på metodparametrar) och adresserna till referensvariabler (i ett minnesområde som kallas "högen") lagras i detta speciella minnesområde. Tillgång till samtalsstacken följer LIFO-principen.

Korrigerat exempel med utgångsvillkor

Låt oss börja med att fixa det andra problemet i koden.

Låt oss försöka lösa problemet direkt: om stapeln är för liten, låt oss göra den större. För att göra detta, starta JVM med flaggan "-Xss" och ange hur mycket minne som ska allokeras för stacken. Låt oss försöka allokera 5 megabyte. Så här kommer det att se ut i IDEA:

Vi lyckades öka längden på produktionen. Nu kan beräkna mer än 49 000 nummer av sekvensen istället för att vara begränsad till cirka 12 000 nummer. Men någon gång får vi fortfarande en StackOverflowError .

Du kan försöka öka storleken på stacken, men det löser inte det grundläggande problemet. Så låt oss leta efter ett problem i logiken. Det måste finnas en punkt när rekursionen upphör. Med andra ord måste det finnas något tillstånd som bestämmer när den rekursiva metoden inte längre kommer att anropas, vilket gör att anropsstacken kan varva ner. För att definiera ett sådant villkor, låt oss göra vårt mål explicit: visa Fibonacci-serien så länge som dess tal är mindre än Integer.MAX_VALUE .

Låt oss skriva en ny printFibonacciWithCondition -metod som tar hänsyn till detta villkor. Och vi kommer att kalla den nya korrigerade metoden i huvudmetoden.


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 att ha kört koden ser vi att utdatan slutar med talet 1836311903. Siffran före denna är 1134903170. Summan av dessa tal är 2_971_215_073, vilket verkligen är större än Integer.MAX_VALUE (2_147_483_647 ) .

Denna ändring åtgärdade automatiskt buggen för långa spill. Om du behöver beräkna mer av serien måste du använda andra datatyper, till exempel BigInteger .

Rekursiv nedstigning och avkoppling

Låt oss analysera steg för steg hur vår kod exekveras. För att göra detta lägger vi till en ekometod och anropar den före och efter det rekursiva anropet av metoden 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);
        }
    }
}
    

Programmet ger oss denna utdata:

0
1
Före metodanrop med args: 0, 1. Aktuellt nummer = 1
Före metodanrop med args: 1, 1. Aktuellt nummer = 2
Före metodanrop med args: 1, 2. Aktuellt nummer = 3
Före metodanrop med args: 2, 3. Nuvarande nummer = 5
Före metodanrop med args: 3, 5. Aktuellt nummer = 8
Före metodanrop med args: 5, 8. Nuvarande nummer = 13
Före metodanrop med args: 8, 13. Aktuellt nummer = 21
Före metodanrop med args: 13, 21. Nuvarande nummer = 34
Före metodanrop med args: 21, 34. Nuvarande nummer = 55
Före metodanrop med args: 34, 55. Nuvarande nummer = 89
Före metodanrop med args: 55, 89. Nuvarande antal = 144
Före metodanrop med args: 89, 144. Nuvarande nummer = 233
Före metodanrop med args: 144, 233. Nuvarande nummer = 377
Före metodanrop med args: 233, 377. Nuvarande nummer = 610
Före metodanrop med args: 377, 610. Nuvarande nummer = 987
Före metodanrop med args: 610, 987. Nuvarande nummer = 1597
Före metodanrop med args: 987, 1597. Nuvarande nummer = 2584
Före metodanrop med args: 1597, 2584. Nuvarande nummer = 4181
Före metoden samtal med args: 2584, 4181. Nuvarande nummer = 6765
Före metod samtal med args: 4181, 6765. Nuvarande nummer = 10946
Före metod samtal med args: 6765, 10946. Nuvarande nummer = 17711
Före metod samtal med args: 179416, 179416. Nuvarande nummer = 28657
Före metodanrop med args: 17711, 28657. Nuvarande nummer = 46368
Före metodanrop med args: 28657, 46368. Nuvarande nummer = 75025
Före metodanrop med args: 46368, 75025. Nuvarande nummer = 12132
args: Före metodanrop med args: 7 121393. Nuvarande nummer = 196418
Före metodanrop med args: 121393, 196418. Aktuellt nummer = 317811
Före metodanrop med args: 196418, 317811. Nuvarande nummer = 514229 Före
metodanrop med 188 args: 295,40 Före: 1317 nummer: 295,40
metod samtal med args: 514229, 832040. Nuvarande nummer = 1346269
Före metod samtal med args: 832040, 1346269. Nuvarande nummer = 2178309
Före metod samtal med args: 1346269, 2178309. Nuvarande nummer = 35245
Före metodanrop med args: 2178309, 3524578. Aktuellt nummer = 5702887
Före metodanrop med args: 3524578, 5702887. Aktuellt nummer = 9227465 Före metodanrop med args: 5702887, 4 Nuvarande metod 3 args 5. 922746 5. 27465
,
14930352. Nuvarande nummer = 24157817
Före metodanrop med args: 14930352, 24157817. Nuvarande nummer = 39088169
Före metodanrop med args: 24157817, 39088169. Nuvarande metod 5 med 86:62:s nummer 9
args, 86:62 3245986. Nuvarande nummer = 102334155
Före metod samtal med args: 63245986, 102334155. Aktuellt nummer = 165580141
Före metodsamtal med args: 102334155, 165580141. Nuvarande nummer = 267914296
Före metodanrop med args: 165580141, 267914296. Aktuellt nummer = 433494437
Före metodanrop med args: 267914296, 433494437. Nuvarande nummer = 701408733 Före metodanrop med args: 4437, 7 Nuvarande nummer = 4437,7. 34903170
Före
metodanrop med args: 701408733, 1134903170. Aktuellt nummer = 1836311903
Efter metodanrop med args: 701408733, 113490317
Efter metodanrop med args: 433494437, 701408733 Efter metodanrop med args: 2967903417, 967903170
med
args: 2967903170, 34494415 580141, 267914296
Efter metodanrop med args: 102334155, 165580141
Efter metodanrop med args: 63245986, 102334155
Efter metodanrop med args: 39088169, 63245986
Efter metodanrop med args: 24157817, 39088169
Efter metodanrop med args: 14930352, 24157817
Efter metodanrop med args: 9227465, 14930352 Efter metodanrop med args: 5702887, 5702887, 5727 efter metod
med args: 5727, 5827
, 5827 887
Efter metodanrop med args : 2178309, 3524578
Efter metodanrop med args: 1346269, 2178309
Efter metodanrop med args: 832040, 1346269 Efter
metodanrop med args: 514229, 832040 Efter metodanrop med args
: 124040, 134629 18
, 317811
Efter metodanrop med args: 121393, 196418
Efter metodanrop med args: 75025, 121393
Efter metodanrop med args: 46368, 75025
Efter metodanrop med args: 28657, 46368
Efter metodanrop med args: 17711, 28657
Efter metodanrop med args: 10946,
17711 Efter metodanrop med args: 6765, 10946
Efter metodanrop med args: 4181,
metodanrop 6765 : 2584, 4181
Efter metodanrop med args: 1597, 2584
Efter metodanrop med args: 987, 1597
Efter metodanrop med args: 610, 987
Efter metodanrop med args: 377, 610
Efter metodanrop med args: 233, 377
Efter metodanrop med args: 144, 233
Efter metodanrop med args: 89, 144
Efter metodanrop med args: 55, 89
Efter metodanrop med args: 34, 55
Efter metodanrop med args: 21, 34
Efter metodanrop med args: 13, 21
Efter metodanrop med args: 8, 13
Efter metodanrop med args: 5, 8
Efter metodanrop med args: 3, 5
Efter metodanrop med args: 2, 3
Efter metodanrop med args: : 1, 2
Efter metodanrop med args: 1, 1
Efter metodanrop med args: 0, 1

Här är en visualisering av vad som händer.

Låt oss säga det igen: metoden printFibonacciWithCondition kallas. Den beräknar det aktuella antalet. Om det passar oss visar vi det och anropar metoden printFibonacciWithCondition igen med nya argument.

Så länge den rekursiva metoden anropas kallas detta för "rekursiv härkomst". När rekursivt avslutas och metodanrop återkommer, säger vi att anropsstacken "vindas av".

Rekursion är ett intressant ämne inom programmering. För att få bättre koll på materialet, låt oss ändra vår uppgift något. Den nya uppgiften är att mata ut, i fallande ordning, alla tal i Fibonacci-serien som inte överstiger heltal.MAX_VALUE . Vi har redan skrivit all kod som behövs för denna uppgift. Allt som återstår är att byta ordning för att visa det aktuella numret och anropa den rekursiva metoden. Det vill säga, i det första exemplet visades det beräknade siffran under "nedstigningen", men nu måste vi "sjunka till botten" och sedan visa siffror "på vägen upp igen". Och naturligtvis, i huvudmetoden , byter vi de två initiala talen i sekvensen (noll och ett) efter att ha visat dem efter att vi anropat den rekursiva metoden. För läsbarheten,metod.


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

Utgången blir:

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