Пример за рекурсивен code без condition за изход

Нека хвърлим нов поглед върху рекурсивен проблем. Като пример, помислете за изчисляване на числата на Фибоначи. Всеки ще си спомни, че редицата на Фибоначи е числова редица, в която първите две числа са 0 и 1, а всяко следващо число е равно на сумата от двете предходни числа.

Нека напишем code за изчисляване и показване на тези числа:


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

Преди първото извикване на рекурсивния метод printFibonacci , отпечатайте първите две числа от редицата: нула и едно. Трябва да направим това, защото рекурсивният метод показва само сумата от входните параметри, а не самите параметри.

Кодът изглежда добре: получаваме две числа, изчисляваме тяхната сума, отпечатваме я на конзолата и извикваме метода printFibonacci отново рекурсивно. Предаваме предишното число (previous) и текущото число (current) като аргументи.

Всъщност codeът има две грешки. Можете да ги видите, ако стартирате codeа.

Първата грешка е препълването на дългия тип. 104-тото число в нашата последователност е отрицателно, което показва, че дългият тип е препълнен.

Втората грешка е различна. След като пресмятаме приблизително 12 000 числа, получаваме:

Изключение в нишка "main" java.lang.StackOverflowError

Сега е подходящ момент да си припомним Howво е стек за извикване на метод в Java. Java машината поддържа запис на всички извиквания на функции. За да направи това, той използва специален вид колекция, наречена стек. Когато една функция извиква друга, Java машината избутва нов StackTraceElement в стека. Когато функцията приключи, този елемент се премахва от стека. Съответно, стекът винаги съхранява актуална информация за текущото състояние на стека за извикване на функции. В documentацията за StackTraceElement се казва, че той е „Изхвърлен, когато възникне препълване на стека, защото приложение рекурсира твърде дълбоко“. Работеща JVM има специална област от паметта за съхраняване на стека за извикване на метод. Размерът на тази област на паметта зависи от настройките на операционната система и JVM. В допълнение към самия стек за извикване на метод, примитивните променливи (специфични стойности на параметрите на метода) и addressите на референтните променливи (в област на паметта, наречена "купчина") се съхраняват в тази специална област на паметта. Достъпът до стека за повиквания следва принципа LIFO.

Коригиран пример с condition за изход

Нека започнем с коригиране на втория проблем в codeа.

Нека се опитаме да решим проблема директно: ако стекът е твърде малък, нека го направим по-голям. За да направите това, стартирайте JVM с флага "-Xss" и укажете колко памет да разпределите за стека. Нека опитаме да разпределим 5 мегаbyteа. Ето How ще изглежда в IDEA:

Успяхме да увеличим дължината на изхода. Сега може да изчисли повече от 49 000 числа от последователността, instead of да бъде ограничен до около 12 000 числа. Но в няHowъв момент все още получаваме StackOverflowError .

Можете да опитате да увеличите размера на стека, но това не решава основния проблем. И така, нека потърсим проблем в логиката. Трябва да има момент, когато рекурсията спира. С други думи, трябва да има няHowво condition, което определя кога рекурсивният метод вече няма да бъде извикан, позволявайки на стека на повикванията да се отвие. За да дефинираме такова condition, нека изясним нашата цел: показване на реда на Фибоначи, стига числата му да са по-малки от Integer.MAX_VALUE .

Нека напишем нов метод printFibonacciWithCondition , който ще вземе предвид това condition. И ще извикаме новия коригиран метод в основния метод.


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

След изпълнение на codeа виждаме, че изходът завършва с числото 1836311903. Числото преди това е 1134903170. Сумата от тези числа е 2_971_215_073, което наистина е по-голямо от Integer.MAX_VALUE (2_147_483_647 ) .

Тази промяна автоматично поправи грешката с дългото препълване. Ако трябва да изчислите повече от сериите, ще трябва да използвате други типове данни, като BigInteger .

Рекурсивно спускане и размотаване

Нека да анализираме стъпка по стъпка How се изпълнява нашият code. За да направим това, ще добавим ехо метод и ще го извикаме преди и след рекурсивното извикване на метода 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);
        }
    }
}
    

Програмата ни дава следния резултат:

0
1
Преди извикване на метод с аргументи: 0, 1. Текущ номер = 1
Преди извикване на метод с аргументи: 1, 1. Текущ номер = 2
Преди извикване на метод с аргументи: 1, 2. Текущ номер = 3
Преди извикване на метод с аргументи: 2, 3. Текущ номер = 5
Преди извикване на метод с аргументи: 3, 5. Текущ номер = 8
Преди извикване на метод с аргументи: 5, 8. Текущ номер = 13
Преди извикване на метод с аргументи: 8, 13. Текущ номер = 21
Преди извикване на метод с аргументи: 13, 21. Текущ номер = 34
Преди извикване на метод с аргументи: 21, 34. Текущ номер = 55
Преди извикване на метод с аргументи: 34, 55. Текущ номер = 89
Преди извикване на метод с аргументи: 55, 89. Текущо число = 144
Преди извикване на метод с аргументи: 89, 144. Текущ номер = 233
Преди извикване на метод с аргументи: 144, 233. Текущ номер = 377
Преди извикване на метод с аргументи: 233, 377. Текущ номер = 610
Преди извикване на метод с аргументи: 377, 610. Текущ номер = 987
Преди извикване на метод с аргументи: 610, 987. Текущ номер = 1597
Преди извикване на метод с аргументи: 987, 1597. Текущ номер = 2584
Преди извикване на метод с аргументи: 1597, 2584. Текущ номер = 4181
Преди метод извикване с аргументи: 2584, 4181. Текущ номер = 6765
Преди извикване на метод с аргументи: 4181, 6765. Текущ номер = 10946
Преди извикване на метод с аргументи: 6765, 10946. Текущ номер = 17711
Преди извикване на метод с аргументи: 10946, 17711. Текущ номер = 28657
Преди извикване на метод с аргументи: 17711, 28657. Текущ номер = 46368
Преди извикване на метод с аргументи: 28657, 46368. Текущ номер = 75025
Преди извикване на метод с аргументи: 46368, 75025. Текущ номер = 121393
Преди извикване на метод с аргументи: 75025, 121393. Текущ номер = 196418
Преди извикване на метод с аргументи: 121393, 196418. Текущ номер = 317811
Преди извикване на метод с аргументи: 196418, 317811. Текущ номер = 514229
Преди извикване на метод с аргументи: 317811, 514229. Текущ номер = 832040
Преди метод извикване с аргументи: 514229, 832040. Текущ номер = 1346269
Преди извикване на метод с аргументи: 832040, 1346269. Текущ номер = 2178309
Преди извикване на метод с аргументи: 1346269, 2178309. Текущ номер = 3524578
Преди извикване на метод с аргументи: 2178309, 3524578. Текущ номер = 5702887
Преди извикване на метод с аргументи: 3524578, 5702887. Текущ номер = 9227465
Преди извикване на метод с аргументи: 5702887, 9227465. Текущ номер = 14930352
Преди извикване на метод с аргументи: 92274 65, 14930352. Текущ номер = 24157817
Преди извикване на метод с аргументи: 14930352, 24157817. Текущ номер = 39088169
Преди извикване на метод с аргументи: 24157817, 39088169. Текущ номер = 63245986
Преди извикване на метод с аргументи: 39088169, 6324 5986. Текущ номер = 102334155
Метод преди извикване с аргументи: 63245986, 102334155. Текущ номер = 165580141
Преди извикване на метод с аргументи: 102334155, 165580141. Текущ номер = 267914296
Преди извикване на метод с аргументи: 165580141, 267914296. Текущ номер = 433494437
Преди извикване на метод с аргументи: 267914296, 433494437. Текущ номер = 701408733 Преди извикване на метод с аргументи: 433494437, 701408733. Текущ
номер = 11349 03170
Преди извикване на метод с аргументи: 701408733, 1134903170. Текущ номер = 1836311903
След извикване на метод с аргументи: 701408733, 113490317
След извикване на метод с аргументи: 433494437, 701408733 След извикване на метод с аргументи: 267914296, 433494437
След извикване на метод с аргументи
: 1655 80141, 267914296
След извикване на метод с аргументи: 102334155, 165580141
След извикване на метод с аргументи: 63245986, 102334155
След извикване на метод с аргументи: 39088169, 63245986
След извикване на метод с аргументи: 24157817, 39088169
След извикване на метод с аргументи: 14930352, 24157817
След извикване на метод с аргументи: 9227465, 14930352 След извикване
на метод с аргументи: 5702887, 9227465
След извикване на метод с аргументи: 3524578, 5702 887
След извикване на метод с аргументи : 2178309, 3524578
След извикване на метод с аргументи: 1346269, 2178309
След извикване на метод с аргументи: 832040, 1346269 След
извикване на метод с аргументи: 514229, 832040 След
извикване на метод с аргументи: 317811, 514229
След извикване на метод с аргументи: 1964 18, 317811
След извикване на метод с аргументи: 121393, 196418
След извикване на метод с аргументи: 75025, 121393
След извикване на метод с аргументи: 46368, 75025
След извикване на метод с аргументи: 28657, 46368
След извикване на метод с аргументи: 17711, 28657
След извикване на метод с аргументи: 10946 , 17711 След извикване
на метод с аргументи: 6765, 10946
След извикване на метод с аргументи: 4181, 6765
След извикване на метод с аргументи : 2584, 4181
След извикване на метод с аргументи: 1597, 2584
След извикване на метод с аргументи: 987, 1597
След извикване на метод с аргументи: 610, 987
След извикване на метод с аргументи: 377, 610
След извикване на метод с аргументи: 233, 377
След извикване на метод с аргументи: 144, 233
След извикване на метод с аргументи: 89, 144
След извикване на метод с аргументи: 55, 89
След извикване на метод с аргументи: 34, 55
След извикване на метод с аргументи: 21, 34
След извикване на метод с аргументи: 13, 21
След извикване на метод с аргументи: 8, 13
След извикване на метод с аргументи: 5, 8
След извикване на метод с аргументи: 3, 5
След извикване на метод с аргументи: 2, 3
След извикване на метод с аргументи : 1, 2
След извикване на метод с аргументи: 1, 1
След извикване на метод с аргументи: 0, 1

Ето визуализация на случващото се.

Нека го кажем отново: извиква се методът printFibonacciWithCondition . Той изчислява текущото число. Ако ни устройва, тогава го показваме и извикваме отново метода printFibonacciWithCondition с нови аргументи.

Докато рекурсивният метод се извиква, това се нарича "рекурсивно спускане". Когато рекурсивното прекратява и извикванията на метод се връщат, казваме, че стекът на извикванията се „развива“.

Рекурсията е интересна тема в програмирането. За да се справим по-добре с материала, нека леко променим задачата си. Новата задача е да се изведат в низходящ ред всички числа от редицата на Фибоначи, които не надвишават Integer.MAX_VALUE . Вече сме написали целия code, необходим за тази задача. Остава само да смените реда на показване на текущия номер и извикване на рекурсивния метод. Тоест в първия пример изчисленото число беше показано по време на „слизането“, но сега трябва да „слезем до самото дъно“ и след това да покажем числа „по пътя обратно нагоре“. И, разбира се, в основния метод разменяме двете начални числа на последователността (нула и едно), след като ги покажем, след като извикаме рекурсивния метод. За четливост,метод.


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

Резултатът ще бъде:

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