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

Давайте ще раз подивимось на рекурсивну задачу. Як приклад можна розглянути пошук чисел Фібоначчі. Хто не пам'ятає, послідовність Фібоначчі – елементи числової послідовності, в якій перші два числа дорівнюють 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