it-swarm.dev

Рекурсия или итерация?

Есть ли снижение производительности, если мы используем цикл вместо рекурсии или наоборот в алгоритмах, где оба могут служить одной и той же цели? Например: проверьте, является ли данная строка палиндромом. Я видел много программистов, использующих рекурсию как способ показать, когда простой итерационный алгоритм может удовлетворить все требования. Играет ли компилятор жизненно важную роль при принятии решения, что использовать?

207
Omnipotent

Возможно, что рекурсия будет более дорогой, в зависимости от того, является ли рекурсивная функция tail recursive (последняя строка - рекурсивный вызов). Хвостовая рекурсия должна распознаваться компилятором и оптимизироваться для его итеративного аналога (при сохранении краткой, ясной реализации, имеющейся в вашем коде).

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

175
Paul Osborne

Циклы могут повысить производительность вашей программы. Рекурсия может повысить производительность вашего программиста. Выберите, что важнее в вашей ситуации!

304
Leigh Caldwell

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

Некоторые алгоритмы просто поддаются рекурсии из-за того, как они спроектированы (последовательности Фибоначчи, обход древовидной структуры и т.д.). Рекурсия делает алгоритм более лаконичным и простым для понимания (следовательно, разделяемым и многократно используемым).

Кроме того, некоторые рекурсивные алгоритмы используют "Ленивая оценка", что делает их более эффективными, чем их итеративные братья. Это означает, что они выполняют дорогостоящие вычисления только в то время, когда они необходимы, а не при каждом запуске цикла.

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

Ссылка 1: Haskel vs PHP (рекурсия против итерации)

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

http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html

Ссылка 2: Освоение рекурсии

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

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

https://developer.ibm.com/articles/l-recurs/

Ссылка 3: Является ли рекурсия более быстрой, чем зацикливание? (Ответ)

Вот ссылка на ответ на вопрос stackoverflow, который похож на ваш. Автор указывает, что многие тесты, связанные либо с рекурсивными, либо с цикличными циклами, очень зависят от языка. Императивные языки обычно быстрее используют цикл и медленнее с рекурсией и наоборот для функциональных языков. Я предполагаю, что основной смысл этой ссылки заключается в том, что очень трудно ответить на вопрос в не зависящем от языка/ситуации слепом смысле.

Является ли рекурсия более быстрой, чем зацикливание?

76
Swift

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

Тем не менее, во многих случаях рекурсия намного более естественна и удобочитаема, чем циклы - как при работе с деревьями. В этих случаях я бы рекомендовал придерживаться рекурсии.

16
Doron Yaacoby

Как правило, можно ожидать ухудшения производительности в другом направлении. Рекурсивные вызовы могут привести к созданию дополнительных кадров стека; штраф за это варьируется. Кроме того, в некоторых языках, таких как Python (точнее, в некоторых реализациях некоторых языков ...), вы можете довольно легко работать с ограничениями стека для задач, которые вы можете указать рекурсивно, например, для поиска максимального значения в древовидная структура данных. В этих случаях вы действительно хотите придерживаться петель.

Написание хороших рекурсивных функций может несколько снизить потери производительности, при условии, что у вас есть компилятор, который оптимизирует хвостовые рекурсии и т.д. (Также дважды проверьте, чтобы убедиться, что функция действительно является хвостовой рекурсивной - это одна из тех ошибок, которые делают многие люди на.)

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

11
zweiterlinde

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

Например, чтобы создать рекурсивный алгоритм Фибоначчи, вы разбиваете fib (n) на fib (n-1) и fib (n-2) и вычисляете обе части. Итерация позволяет вам повторять одну и ту же функцию снова и снова.

Тем не менее, Фибоначчи на самом деле является ошибочным примером, и я думаю, что итерация на самом деле более эффективна. Обратите внимание, что fib (n) = fib (n-1) + fib (n-2) и fib (n-1) = fib (n-2) + fib (n-3). FIB (N-1) рассчитывается дважды!

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

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

8
Ben

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

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

7
entzik

Я считаю, что хвостовая рекурсия в Java в настоящее время не оптимизирована. Подробности разбросаны по всему это обсуждение LtU и связанных ссылок. Он может быть функцией в следующей версии 7, но, очевидно, он представляет определенные трудности в сочетании с проверкой стека, поскольку некоторые кадры будут отсутствовать. Проверка стека используется для реализации их детальной модели безопасности начиная с Java 2.

http://lambda-the-ultimate.org/node/13

6
Mike Edwards

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

Итеративная реализация

public static void sort(Comparable[] a)
{
    int N = a.length;
    aux = new Comparable[N];
    for (int sz = 1; sz < N; sz = sz+sz)
        for (int lo = 0; lo < N-sz; lo += sz+sz)
            merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}

Рекурсивная реализация

private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
    if (hi <= lo) return;
    int mid = lo + (hi - lo) / 2;
    sort(a, aux, lo, mid);
    sort(a, aux, mid+1, hi);
    merge(a, aux, lo, mid, hi);
}

PS - это то, что рассказал профессор Кевин Уэйн (Принстонский университет) на курсе об алгоритмах, представленных на Coursera.

5
Nikunj Banka

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

5
Felix

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

int factorial ( int input )
{
  int x, fact = 1;
  for ( x = input; x > 1; x--)
     fact *= x;
  return fact;
}

Теперь рассмотрим это с помощью рекурсивной функции

int factorial ( int input )
{
  if (input == 0)
  {
     return 1;
  }
  return input * factorial(input - 1);
}

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

5
Harikrishnan

Это зависит от языка. В Java вы должны использовать циклы. Функциональные языки оптимизируют рекурсию.

4
mc

Используя рекурсию, вы несете стоимость вызова функции с каждой "итерацией", тогда как с циклом единственное, что вы обычно платите, это увеличение/уменьшение. Таким образом, если код для цикла не намного сложнее, чем код для рекурсивного решения, цикл обычно будет лучше, чем рекурсия.

4
MovEaxEsp

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

4
Warrior

Рекурсия является более простой (и, следовательно, более фундаментальной), чем любое возможное определение итерации. Вы можете определить полную по Тьюрингу систему только с пара комбинаторов (да, даже рекурсия сама по себе является производным понятием в такой системе). лямбда исчисление - не менее мощная фундаментальная система с рекурсивными функциями. Но если вы хотите правильно определить итерацию, вам понадобится гораздо больше примитивов для начала.

Что касается кода - нет, рекурсивный код на самом деле гораздо легче понять и поддерживать, чем чисто итеративный, поскольку большинство структур данных являются рекурсивными. Конечно, чтобы сделать это правильно, нужен язык с поддержкой функций и замыканий высшего порядка, по крайней мере, для аккуратного получения всех стандартных комбинаторов и итераторов. Конечно, в C++ сложные рекурсивные решения могут выглядеть немного безобразно, если только вы не хардкорный пользователь FC++ и тому подобное.

3
SK-logic

Если вы просто перебираете список, то, конечно же, перебираете.

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

Проверьте методы поиска здесь: http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html

3
Joe Cheng

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

2
metadave

В C++, если рекурсивная функция является шаблонной, тогда у компилятора больше шансов оптимизировать ее, так как все дедукции типов и реализации функций будут происходить во время компиляции. Современные компиляторы также могут встроить функцию, если это возможно. Таким образом, если в -O3 используются флаги оптимизации, такие как -O2 или g++, то рекурсии могут иметь шанс быть быстрее, чем итерации. В итерационных кодах у компилятора меньше шансов оптимизировать его, поскольку он уже находится в более или менее оптимальном состоянии (если он написан достаточно хорошо).

В моем случае я пытался реализовать возведение в матрицу путем возведения в квадрат с использованием матричных объектов Armadillo как рекурсивным, так и итерационным способом. Алгоритм можно найти здесь ... https://en.wikipedia.org/wiki/Exponentiation_by_squaring . Мои функции были шаблонными, и я вычислил 1,000,00012x12 матрицы, возведенные в степень 10. Я получил следующий результат:

iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec

iterative + No-optimisation flag  -> 2.83.. sec
recursive + No-optimisation flag  -> 4.15.. sec

Эти результаты были получены с использованием gcc-4.8 с флагом c ++ 11 (-std=c++11) и Armadillo 6.1 с Intel mkl. Компилятор Intel также показывает аналогичные результаты.

2
Titas Chanda

Рекурсия? С чего начать, вики скажет вам: "Это процесс повторения предметов самоподобным способом"

Когда-то, когда я делал C, рекурсия C++ была просто идеей, вроде "Хвостовой рекурсии". Вы также найдете много алгоритмов сортировки, использующих рекурсию. Пример быстрой сортировки: http://alienryderflex.com/quicksort/

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

2
Nickz

это зависит от "глубины рекурсии". это зависит от того, насколько накладные расходы на вызов функции будут влиять на общее время выполнения.

Например, вычисление классического факториала рекурсивным способом очень неэффективно из-за: - риска переполнения данных - риска переполнения стека - издержки вызова функции занимают 80% времени выполнения

в то время как разработка алгоритма min-max для анализа позиции в игре в шахматы, которая будет анализировать последующие N ходов, может быть реализована в рекурсии по "глубине анализа" (как я делаю ^ _ ^)

2
ugasoft

Майк прав. Хвостовая рекурсия не оптимизирована компилятором Java или JVM. Вы всегда получите переполнение стека чем-то вроде этого:

int count(int i) {
  return i >= 100000000 ? i : count(i+1);
}
1
noah

Недостаток рекурсии состоит в том, что алгоритм, который вы пишете с использованием рекурсии, имеет сложность O(n). В то время как итеративный подход имеет пространственную сложность O (1). Это преимущество использования итерации над рекурсией. Тогда почему мы используем рекурсию?

Увидеть ниже.

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

1
Varunnuevothoughts

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

1
Grigori A.

Насколько я знаю, Perl не оптимизирует хвостовые рекурсивные вызовы, но вы можете подделать его.

sub f{
  my($l,$r) = @_;

  if( $l >= $r ){
    return $l;
  } else {

    # return f( $l+1, $r );

    @_ = ( $l+1, $r );
    goto &f;

  }
}

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

Обратите внимание, что "my @_;" или "local @_;" не существует, если вы это сделаете, это больше не будет работать.

0
Brad Gilbert

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

Например, в некоторых языках существуют рекурсивные реализации многопоточной сортировки слиянием.

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

0
ccpizza

Используя всего лишь Chrome 45.0.2454.85 м, рекурсия кажется хорошей суммой быстрее.

Вот код:

(function recursionVsForLoop(global) {
    "use strict";

    // Perf test
    function perfTest() {}

    perfTest.prototype.do = function(ns, fn) {
        console.time(ns);
        fn();
        console.timeEnd(ns);
    };

    // Recursion method
    (function recur() {
        var count = 0;
        global.recurFn = function recurFn(fn, cycles) {
            fn();
            count = count + 1;
            if (count !== cycles) recurFn(fn, cycles);
        };
    })();

    // Looped method
    function loopFn(fn, cycles) {
        for (var i = 0; i < cycles; i++) {
            fn();
        }
    }

    // Tests
    var curTest = new perfTest(),
        testsToRun = 100;

    curTest.do('recursion', function() {
        recurFn(function() {
            console.log('a recur run.');
        }, testsToRun);
    });

    curTest.do('loop', function() {
        loopFn(function() {
            console.log('a loop run.');
        }, testsToRun);
    });

})(window);

РЕЗУЛЬТАТЫ

// 100 прогонов, используя стандартный цикл for

100x для цикла. Время завершения: 7,683мс

// 100 прогонов с использованием функционально-рекурсивного подхода с хвостовой рекурсией

100-кратный рекурсивный прогон. Время на завершение: 4.841мс

На приведенном ниже снимке экрана рекурсия снова побеждает с большим отрывом, когда выполняется при 300 циклах на тест

Recursion wins again!

0
Alpha G33k