Самый быстрый выход из рекурсии в Java

Есть ли способ для быстрого и быстрого выхода из рекурсии на Java? Существует способ выйти из for loop с помощью оператора break;. Есть ли эквивалентный шаблон или метод выхода из рекурсии?

Я могу думать о том, чтобы создать отдельный поток, и как только значение вычисляется, просто убивая поток, а не пузырясь в стеке рекурсии. Есть ли лучший способ?

Уже есть вопрос, который обсуждает, как вы можете вырваться из рекурсии: здесь.

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

Критериями, рассмотренными здесь, являются:

  • Легкость рефакторинга для использования этого escape-кода
  • Сырая производительность (чем быстрее, тем лучше)
  • Длина на практике (чем быстрее написать/добавить лучшее)

Ответ, который я ищу, объяснит как производительность, так и простоту решения - это задается в контексте алгоритмического соревнования, поэтому предпочтительны решения, требующие меньше рефакторинга.

Почему я хочу использовать его?

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

public static MyClass myFunct(MyClass c, int x){
    myFunct(c, c.valueA);
    myFunct(c, c.valueB);
    //do some work - that modifies class c
    if(c.valueE == 7){
        //finished work, can return without unwinding the whole recursion
        //No more modifications to class c
    }
    myFunct(c, c.valueC);
    myFunct(c, c.valueD);
    return c;
}

Ответы

Ответ 1

Ответ на ваш вопрос довольно прост:

Просто делайте это обычным способом, т.е. самостоятельно разматывая стек с помощью return s. Зачем? Потому что это не так медленно, как вы думаете. Если вычисление в вашей рекурсии очень тривиально, а глубина стека очень высока, возвращение никогда не будет влиять на время выполнения вашего алгоритма заметно.

В принципе, у вас есть следующие возможности:

  • Если возможно, преобразуйте свой алгоритм в итеративный.
  • Превратите свой алгоритм в конец рекурсивного и надейтесь, что виртуальная машина повторно использует фрейм стека. Тогда возврат из рекурсии практически равен одному простому возврату.
  • Выбросьте исключение. Тем не менее, это будет еще медленнее, чем возвращение, потому что должна быть построена трассировка стека, которая тоже увлекает стеки. Также необходимо отключить стек для проверки catch es. Вы ничего не выигрываете.

Первые два варианта жизнеспособны, но не всегда возможны. Но, честно говоря, не думай об этом. Возвращение из глубокого стека не является частью, которая замедляет ваш алгоритм. Если у вас есть алгоритм с очень глубокой рекурсией, тогда у вас есть проблема в любом случае (переполнение стека, затраты на рекурсивные вызовы) и следует рассмотреть возможность переписать ваш алгоритм. Если глубина стека невелика, это в любом случае не является проблемой.

Вот простая тестовая программа Java, чтобы показать вам, что я имею в виду:

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

public class DeepRecursion {

    private long returnStartTime;

    public int recurse(int i) {
        int r = (int)Math.sqrt((double)i); // Some non-trivial computation
        if(i == 0) {
            returnStartTime = System.currentTimeMillis();
            return r;
        }
        return r + recurse(i-1);
    }

    public void testRecursion(int i, PrintStream p) {
        long startTime = System.currentTimeMillis();
        int result = recurse(i);
        long endTime = System.currentTimeMillis();

        p.println(
                "Recursion depth: " + i + " Result: " + result + "\n" +
                "Time for recursion" + (returnStartTime - startTime) + "\n" +
                "Time for return " + (endTime - returnStartTime) + "\n"
                );
    }

    public void testIteration(int i, PrintStream p) {
        long startTime = System.currentTimeMillis();
        int result = 0;
        for(int k = 0; k <= i; k++) {
            int r = (int)Math.sqrt((double)k); // Some non-trivial computation
            result += r;
        }
        long endTime = System.currentTimeMillis();
        p.println("Iteration length: " + i + " Result: " + result + "\nTime: " + (endTime - startTime) );
    }

    public static void main(String[] args) {
        DeepRecursion r = new DeepRecursion();
        PrintStream nullStream = new PrintStream(new ByteArrayOutputStream());

        for(int k = 0; k < 10; k++) {
            // Test stack depths from 1024 to 33554432
            for(int i = 10; i < 26; i++) {
                r.testIteration(1 << i, k == 9 ? System.out : nullStream);
                r.testRecursion(1 << i, k == 9 ? System.out : nullStream);
            }
        }
    }
}

Он вычисляет рекурсивную функцию, которая будет иметь глубину стека, равную входному параметру. Функция вычисляет квадратный корень в каждом кадре стека e, чтобы имитировать некоторые нетривиальные вычисления. Он также вычисляет одну и ту же функцию итерационным способом. Чтобы разогреть JIT, программа сначала выполняется 9 раз без печати результата; печатается только десятый результат. Вот мои результаты (мне пришлось увеличить размер стека до 1 гигабайта с помощью -Xss1g. Вот результаты с моей машины:

Iteration length: 1024 Result: 21360
Time for iteration: 0
Recursion depth: 1024 Result: 21360
Time for recursion 0
Time for return 0

Iteration length: 2048 Result: 60810
Time for iteration: 0
Recursion depth: 2048 Result: 60810
Time for recursion 0
Time for return 0

Iteration length: 4096 Result: 172768
Time for iteration: 0
Recursion depth: 4096 Result: 172768
Time for recursion 0
Time for return 0

Iteration length: 8192 Result: 490305
Time for iteration: 0
Recursion depth: 8192 Result: 490305
Time for recursion 0
Time for return 0

Iteration length: 16384 Result: 1390016
Time for iteration: 0
Recursion depth: 16384 Result: 1390016
Time for recursion 0
Time for return 0

Iteration length: 32768 Result: 3938198
Time for iteration: 0
Recursion depth: 32768 Result: 3938198
Time for recursion 0
Time for return 0

Iteration length: 65536 Result: 11152256
Time for iteration: 0
Recursion depth: 65536 Result: 11152256
Time for recursion 1
Time for return 0

Iteration length: 131072 Result: 31570201
Time for iteration: 0
Recursion depth: 131072 Result: 31570201
Time for recursion 1
Time for return 0

Iteration length: 262144 Result: 89347840
Time for iteration: 2
Recursion depth: 262144 Result: 89347840
Time for recursion 1
Time for return 1

Iteration length: 524288 Result: 252821886
Time for iteration: 2
Recursion depth: 524288 Result: 252821886
Time for recursion 4
Time for return 1

Iteration length: 1048576 Result: 715304448
Time for iteration: 5
Recursion depth: 1048576 Result: 715304448
Time for recursion 7
Time for return 3

Iteration length: 2097152 Result: 2023619820
Time for iteration: 9
Recursion depth: 2097152 Result: 2023619820
Time for recursion 14
Time for return 4

Iteration length: 4194304 Result: 1429560320
Time for iteration: 18
Recursion depth: 4194304 Result: 1429560320
Time for recursion 29
Time for return 12

Iteration length: 8388608 Result: -986724456
Time for iteration: 36
Recursion depth: 8388608 Result: -986724456
Time for recursion 56
Time for return 28

Iteration length: 16777216 Result: -1440040960
Time for iteration: 72
Recursion depth: 16777216 Result: -1440040960
Time for recursion 112
Time for return 61

Iteration length: 33554432 Result: 712898096
Time for iteration: 145
Recursion depth: 33554432 Result: 712898096
Time for recursion 223
Time for return 123

Как вы видите, для возврата из стопки глубины один миллион требуется 3 миллисекунды. Большие размеры стека вызывают более длительное время, возможно, из-за того, что стек больше не подходит для кеша L3. Однако, если вам нужны такие большие стеки, у вас есть проблема в любом случае, как описано выше. Запуск Java с максимальным размером стека 1 гигабайт - не лучшая идея. Любой размер стека ниже 131072 даже не измеряется в миллисекундах. В разумном алгоритме стек должен быть намного меньше этого, поэтому вы всегда должны быть в порядке.

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

Заключение

Если рекурсия слишком медленная для вас, полностью избавитесь от нее. Просто пропустить возвращение не будет иметь большого значения.

Ответ 2

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

рекурсии:

int rec(int i){
    if(i == condition)
         return i;

    //do some stuff with i

    return rec(i);
}

нерекурсивна:

int nonrec(int i){
    Stack<Integer> stack = new Stack<>();
    stack.push(i);

    while(!stack.isEmpty())
    {
        Integer v = stack.pop();

        //same as above, but no bubbling through the stack
        if(v == condition)
            return v;

        //do some stuff with v

        stack.push(v);
    }

    //undefined, the algorithm hasn't found a solution
    return -1;
}

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

Ответ 3

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

public A f(B b) {
    C c = new C();
    return recf(b, c);
}

private A recf(B b, C c) {
    ...
    A a = recf(b2, c2);
    if (a != null) { // found
        return a;
    }
    ...
    return recf(b3, c3);
}

Это приведет к получению последовательности возвратов при обнаружении (a != null).

Аналогия с break является Throwable.

public A f(B b) {
    C c = new C();
    try (
        recf(b, c);
        return null; // not found
    } catch (ResultFoundException<A> e) {
        return e.getResult();
    }
}

private void recf(B b, C c) throws ResultFoundException<A> {
    ...
}

public class ResultFoundException<A> implements RuntimeException {
    ...

    /** Speed-up thanks to James_pic. */
    @Override
    public Throwable fillInStackTrace() { }
}

Ответ 4

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

Ответ 5

Как было указано другими людьми, стек должен быть перемотан.

Наличие другого потока является уродливым и, вероятно, медленнее, чем исключение.

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

Если бы мне нужно было это сделать отчаянно, у меня был бы метод оболочки, который ловит исключение, созданное рекурсивной функцией.

Отказ от ответственности: я не пробовал это, поэтому он даже не может скомпилировать:)

class GotAResultException extends Exception {
  private Object theResult;
  public GotAResultException(Object theResult) {
    this.theResult = theResult;
  }
  public Object getResult() {
    return theResult;
  }
}

Object wrapperMethod(Object blaParameters) {
  try {
    recursiveMethod(blaParameters);
  } catch (GotAResultException e) {
    return e.getResult();
  }
  // maybe return null if no exception was thrown?
  // Your call
  return null;
}

void recursiveMethod(Object blaParameters) throws GotAResultException {
  // lots of magical code
  // that calls recursiveMethod recursivelly
  // ...
  // And then we found the solution!
  throw new GotAResultException("this is the result");
}

Ответ 6

Широко признанный дизайн - даже если он может зависеть от ваших конкретных обстоятельств - использует Java Future <T> (или похожие). Если вы собираетесь использовать потоки, имейте в виду, что для отмены потока требуется безопасная и чистая политика. Там очень хорошая литература об этих предметах, например. Java Concurrency На практике.

Ответ 7

Один из вариантов - установить флаг на MyClass и вернуться на основе этого:

public static MyClass myFunct(MyClass c, int x){
    if (c.isDoneCalculating) {
        return c;
    }
    myFunct(c, c.valueA);
    myFunct(c, c.valueB);
    //do some work - that modifies class c
    if(c.valueE == 7){
        c.isDoneCalculating = true;
        //finished work, can return without unwinding the whole recursion
        //No more modifications to class c
    }
    myFunct(c, c.valueC);
    myFunct(c, c.valueD);
    return c;
}