Рекурсия в Java

Рекурсия на Яве

1. Вступление

В этой статье мы сосредоточимся на ключевой концепции любого языка программирования - рекурсии.

Мы объясним характеристики arecursive function и покажем, как использовать рекурсию для решения различных проблем в Java.

2. Понять рекурсию

2.1. Определение

В Java механизм вызова функций поддерживаетthe possibility of having a method call itself. Эта функция известна какrecursion..

Например, предположим, что мы хотим суммировать целые числа от 0 до некоторого значенияn:

public int sum(int n) {
    if (n >= 1) {
        return sum(n - 1) + n;
    }
    return n;
}

Есть два основных требования к рекурсивной функции:

  • A Stop Condition - функция возвращает значение при выполнении определенного условия без дальнейшего рекурсивного вызова

  • The Recursive Call - функция вызывает себя сinput, что на шаг ближе к условию остановки

Каждый рекурсивный вызов добавляет новый кадр в стековую память JVM. Итак,if we don’t pay attention to how deep our recursive call can dive, an out of memory exception may occur.

Эту потенциальную проблему можно предотвратить, используя оптимизацию хвостовой рекурсии.

2.2. Рекурсия хвоста против рекурсии головы

Мы называем рекурсивную функциюtail-recursionwhen the recursive call is the last thing that function executes. В противном случае она известна какhead-recursion.

Наша реализация выше функцииsum() является примером рекурсии головы и может быть изменена на рекурсию хвоста:

public int tailSum(int currentSum, int n) {
    if (n <= 1) {
        return currentSum + n;
    }
    return tailSum(currentSum + n, n - 1);
}

С хвостовой рекурсиейthe recursive call is the last thing the method does, so there is nothing left to execute within the current function.

Таким образом, логически нет необходимости хранить фрейм стека текущей функции.

Хотя компиляторcan использует эту точку для оптимизации памяти, следует отметить, что файлJava compiler doesn’t optimize for tail-recursion for now.

2.3. Рекурсия против итерации

Рекурсия может помочь упростить реализацию некоторых сложных проблем, сделав код более понятным и читабельным.

Но, как мы уже видели, рекурсивный подход часто требует больше памяти, поскольку требуемая память стека увеличивается с каждым рекурсивным вызовом.

В качестве альтернативы, если мы можем решить проблему с рекурсией, мы также можем решить ее итерацией.

Например, наш методsum может быть реализован с использованием итерации:

public int iterativeSum(int n) {
    int sum = 0;
    if(n < 0) {
        return -1;
    }
    for(int i=0; i<=n; i++) {
        sum += i;
    }
    return sum;
}

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

Правильный выбор между рекурсией головы, хвостовой рекурсией и итеративным подходом зависит от конкретной проблемы и ситуации.

3. Примеры

А теперь давайте попробуем решить некоторые проблемы рекурсивным способом.

3.1. Нахождение n-й степени десяти

Предположим, нам нужно вычислитьn-ю степень 10. Здесь мы вводимn.. Думая рекурсивно, мы можем сначала вычислить(n-1)-ю степень 10 и умножить результат на 10.

Затем, чтобы вычислить(n-1)-ю степень 10, будет(n-2)-ю степенью 10 и умножить этот результат на 10, и так далее. Мы будем продолжать так, пока не дойдем до точки, где нам нужно вычислить (n-n) -ю степень 10, которая равна 1.

Если бы мы хотели реализовать это на Java, мы бы написали:

public int powerOf10(int n) {
    if (n == 0) {
        return 1;
    }
    return powerOf10(n-1) * 10;
}

3.2. Нахождение n-го элемента последовательности Фибоначчи

Начиная с0 и1,the Fibonacci Sequence is a sequence of numbers where each number is defined as the sum of the two numbers proceeding it:0 1 1 2 3 5 8 13 21 34 55

Итак, учитывая числоn, наша задача состоит в том, чтобы найтиn-й элементFibonacci Sequence. To implement a recursive solution, we need to figure out the Stop Condition and the*Recursive Call*.

К счастью, это действительно просто.

Назовемf(n) значениемn в последовательности. Тогда у нас будетf(n) = f(n-1) + f(n-2) (theRecursive Call).

Между тем,f(0) = 0 иf(1) = 1 (Stop Condition).

Тогда для нас действительно очевидно, что нужно определить рекурсивный метод решения проблемы:

public int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n-1) + fibonacci(n-2);
}

3.3. Преобразование из десятичного в двоичное

Теперь давайте рассмотрим проблему преобразования десятичного числа в двоичное. Требуется реализовать метод, который получает положительное целочисленное значениеn и возвращает двоичное представлениеString.

Один из подходов к преобразованию десятичного числа в двоичное состоит в том, чтобы разделить значение на 2, записать остаток и продолжить делить частное на 2.

Мы продолжаем так делить, пока не получим частное0. Затем, выписав все остатки в резервном порядке, мы получим двоичную строку.

Следовательно, наша проблема состоит в том, чтобы написать метод, который возвращает эти остатки в резервном порядке:

public String toBinary(int n) {
    if (n <= 1 ) {
        return String.valueOf(n);
    }
    return toBinary(n / 2) + String.valueOf(n % 2);
}

3.4. Высота двоичного дерева

Высота бинарного дерева определяется как количество ребер от корня до самого глубокого листа. Наша задача сейчас - вычислить это значение для данного двоичного дерева.

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

Но пытаясь придумать рекурсивное решение, мы можем переформулировать определение высоты двоичного дерева как максимальную высоту левой ветви корня и правой ветви корня плюс1.

Если у корня нет левой и правой ветвей, его высота составляетzero.

Вот наша реализация:

public int calculateTreeHeight(BinaryNode root){
    if (root!= null) {
        if (root.getLeft() != null || root.getRight() != null) {
            return 1 +
              max(calculateTreeHeight(root.left),
                calculateTreeHeight(root.right));
        }
    }
    return 0;
}

Отсюда видим, чтоsome problems can be solved with recursion in a really simple way.

4. Заключение

В этом руководстве мы представили концепцию рекурсии в Java и продемонстрировали ее на нескольких простых примерах.

Реализацию этой статьи можно найтиover on Github.