Пример кода на рекурсию без условия выхода

Давайте еще раз посмотрим на рекурсивную задачу. В качестве примера можно рассмотреть поиск чисел Фибоначчи. Кто не помнит, последовательность Фибоначчи – элементы числовой последовательности, в которой первые два числа равны 0 и 1, а каждое последующее число равно сумме двух предыдущих чисел.

Напишем код для поиска и вывода таких чисел:


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).

На самом деле в этом коде есть 2 ошибки. Их можно заметить, если запустить код.

Первая ошибка заключается в переполнении типа long. Уже 104-е число в нашей последовательности вывелось отрицательным, а это значит, что произошло переполнение типа long.

Вторая ошибка имеет другой характер. После найденного 12 тысяч с копейками числа, на экран выводится:

Exception in thread "main" java.lang.StackOverflowError

Здесь уместно вспомнить, что такое стек вызова методов в Java. Java-машина ведет запись всех вызовов функций. У нее есть для этого специальная коллекция – стек (Stack). Когда одна функция вызывает другую, Java-машина помещает в этот стек новый элемент StackTraceElement. Когда функция завершается, этот элемент удаляется из стека. Таким образом в этом стеке всегда хранится актуальная информация о текущем состоянии "стека вызовов функций". Дословный перевод ошибки StackOverflowError – "стек переполнен". В Javadoc-е написано: "Бросается, когда стек вызова слишком глубокий". В запущенной JVM есть специальная область памяти для хранения стека вызова методов. Размер этой области памяти зависит от ОС и настроек JVM. Кроме самого стека вызовов методов в этой области памяти хранятся примитивные переменные (конкретные значения параметров вызова метода) и адреса ссылочных переменных (в HEAP области памяти). Модель доступа к стеку – LIFO.

Исправленный пример с условием выхода

Исправления нашего кода начнем со второй проблемы.

Попробуем проблему решить в лоб: если маленький размер стека, то давайте его увеличим. Для этого нужно запустить JVM со специальным флагом "-Xss" и указать сколько выделить памяти под стек. Давай попробуем выделить 5 мегабайт. Выглядеть это в IDEA будет так:

Да, длина вывода увеличилась и сейчас составляет не 12+ тысяч найденных чисел последовательности, а 49+ тысяч. Но после какого-то числа все равно получаем StackOverflowError.

Можно пробовать еще увеличивать стек-область памяти, но принципиально ничего не изменится. Значит, будем искать проблему в логике. У рекурсии должна быть точка остановки. То есть должно быть какое-то условие, после которого рекурсивный метод не вызовется, и стек вызова будет возвращаться. Для того, чтобы определить такое условие, давайте конкретизируем задачу – выводить числовой ряд Фибоначчи до тех пор, пока они меньше, чем Integer.MAX_VALUE.

Напишем новый метод printFibonacciWithCondition, в котором учтем это условие. И в методе main вызовем именно новый исправленный метод.


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

После запуска кода действительно видим, что вывод завершился числом 1836311903. Перед этим числом было 1134903170. Их сумма 2_971_215_073, что действительно больше Integer.MAX_VALUE (2_147_483_647).

Вместе с этим исправлением у нас автоматически исправилась ошибка с переполнением типа long. Если нужен более длинный числовой ряд, нужно использовать другие типы данных, например BigInteger.

Метод рекурсивного спуска и возврата

Давайте попробуем поэтапно проанализировать как выполняется наш код. Для это добавим метод echo и будем его вызывать перед и после рекурсивного вызова метода 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
Before method call with args: 0, 1. Current number = 1
Before method call with args: 1, 1. Current number = 2
Before method call with args: 1, 2. Current number = 3
Before method call with args: 2, 3. Current number = 5
Before method call with args: 3, 5. Current number = 8
Before method call with args: 5, 8. Current number = 13
Before method call with args: 8, 13. Current number = 21
Before method call with args: 13, 21. Current number = 34
Before method call with args: 21, 34. Current number = 55
Before method call with args: 34, 55. Current number = 89
Before method call with args: 55, 89. Current number = 144
Before method call with args: 89, 144. Current number = 233
Before method call with args: 144, 233. Current number = 377
Before method call with args: 233, 377. Current number = 610
Before method call with args: 377, 610. Current number = 987
Before method call with args: 610, 987. Current number = 1597
Before method call with args: 987, 1597. Current number = 2584
Before method call with args: 1597, 2584. Current number = 4181
Before method call with args: 2584, 4181. Current number = 6765
Before method call with args: 4181, 6765. Current number = 10946
Before method call with args: 6765, 10946. Current number = 17711
Before method call with args: 10946, 17711. Current number = 28657
Before method call with args: 17711, 28657. Current number = 46368
Before method call with args: 28657, 46368. Current number = 75025
Before method call with args: 46368, 75025. Current number = 121393
Before method call with args: 75025, 121393. Current number = 196418
Before method call with args: 121393, 196418. Current number = 317811
Before method call with args: 196418, 317811. Current number = 514229
Before method call with args: 317811, 514229. Current number = 832040
Before method call with args: 514229, 832040. Current number = 1346269
Before method call with args: 832040, 1346269. Current number = 2178309
Before method call with args: 1346269, 2178309. Current number = 3524578
Before method call with args: 2178309, 3524578. Current number = 5702887
Before method call with args: 3524578, 5702887. Current number = 9227465
Before method call with args: 5702887, 9227465. Current number = 14930352
Before method call with args: 9227465, 14930352. Current number = 24157817
Before method call with args: 14930352, 24157817. Current number = 39088169
Before method call with args: 24157817, 39088169. Current number = 63245986
Before method call with args: 39088169, 63245986. Current number = 102334155
Before method call with args: 63245986, 102334155. Current number = 165580141
Before method call with args: 102334155, 165580141. Current number = 267914296
Before method call with args: 165580141, 267914296. Current number = 433494437
Before method call with args: 267914296, 433494437. Current number = 701408733
Before method call with args: 433494437, 701408733. Current number = 1134903170
Before method call with args: 701408733, 1134903170. Current number = 1836311903
After method call with args: 701408733, 113490317
After method call with args: 433494437, 701408733
After method call with args: 267914296, 433494437
After method call with args: 165580141, 267914296
After method call with args: 102334155, 165580141
After method call with args: 63245986, 102334155
After method call with args: 39088169, 63245986
After method call with args: 24157817, 39088169
After method call with args: 14930352, 24157817
After method call with args: 9227465, 14930352
After method call with args: 5702887, 9227465
After method call with args: 3524578, 5702887
After method call with args: 2178309, 3524578
After method call with args: 1346269, 2178309
After method call with args: 832040, 1346269
After method call with args: 514229, 832040
After method call with args: 317811, 514229
After method call with args: 196418, 317811
After method call with args: 121393, 196418
After method call with args: 75025, 121393
After method call with args: 46368, 75025
After method call with args: 28657, 46368
After method call with args: 17711, 28657
After method call with args: 10946, 17711
After method call with args: 6765, 10946
After method call with args: 4181, 6765
After method call with args: 2584, 4181
After method call with args: 1597, 2584
After method call with args: 987, 1597
After method call with args: 610, 987
After method call with args: 377, 610
After method call with args: 233, 377
After method call with args: 144, 233
After method call with args: 89, 144
After method call with args: 55, 89
After method call with args: 34, 55
After method call with args: 21, 34
After method call with args: 13, 21
After method call with args: 8, 13
After method call with args: 5, 8
After method call with args: 3, 5
After method call with args: 2, 3
After method call with args: 1, 2
After method call with args: 1, 1
After method call with args: 0, 1

Давайте графически проиллюстрируем, как это происходит.

Проговорим еще раз: вызывается метод printFibonacciWithCondition. В нем вычисляется текущее число. Если оно подходит нам, то выводим его и снова вызываем метод printFibonacciWithCondition с новыми параметрами.

Пока идет вызов рекурсивного метода – это называется "Рекурсивный спуск". Когда идет возврат по стеку вызова – "Рекурсивный возврат".

Рекурсия – интересная тема в программировании. Для лучшего усвоения материала немного изменим нашу задачу. Нужно вывести ряд чисел Фибоначчи, которые не превышают Integer.MAX_VALUE в убывающем порядке. Для того, чтоб решить эту задачу, у нас уже написан весь код. Все что остается – это поменять местами вывод текущего числа и вызов рекурсивного метода. То есть в первом примере вывод найденного числа происходил "на этапе спуска", а сейчас нам нужно "спуститься в самый низ" и выводить числа на этапе "возврата". Ну и конечно, в методе main вывод двух начальных чисел последовательности (ноль и единица) поменять местами и вывести после вызова рекурсивного метода. Для читаемости удалим метод echo.


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