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

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

197
задан 16 сент. '08 в 16:33
источник поделиться
28 ответов

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

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

171
ответ дан 16 сент. '08 в 16:52
источник

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

290
ответ дан 16 сент. '08 в 17:11
источник

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

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

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

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

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

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

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

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

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

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

http://www.ibm.com/developerworks/linux/library/l-recurs/index.html

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

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

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

75
ответ дан 24 июня '11 в 6:47
источник

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

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

16
ответ дан 16 сент. '08 в 16:43
источник

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

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

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

11
ответ дан 16 сент. '08 в 16:45
источник

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

Например, чтобы сделать рекурсивный алгоритм Фибоначи, вы разбиваете 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) вычисляется дважды!

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

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

8
ответ дан 24 июня '11 в 7:18
источник

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

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

7
ответ дан 16 сент. '08 в 16:55
источник

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

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

6
ответ дан 16 сент. '08 в 16:56
источник

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

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
ответ дан 24 июня '11 в 6:46
источник

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

5
ответ дан 24 июня '11 в 6:39
источник

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

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

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
ответ дан 08 дек. '12 в 9:26
источник

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

4
ответ дан 16 сент. '08 в 16:38
источник

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

4
ответ дан 29 нояб. '08 в 9:23
источник

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

4
ответ дан 16 сент. '08 в 16:35
источник

Если вы просто перебираете список, то уверен, идите дальше.

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

Ознакомьтесь с методами "find" здесь: http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html

3
ответ дан 24 июня '11 в 7:01
источник

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

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

3
ответ дан 24 июня '11 в 13:33
источник

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

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

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

2
ответ дан 16 сент. '08 в 16:48
источник

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

2
ответ дан 16 сент. '08 в 16:39
источник

рекурсии? С чего начать, вики расскажут вам "процесс повторения элементов автомодельно"

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

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

2
ответ дан 24 июня '11 в 6:59
источник

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

В моем случае я пытался реализовать возведение в степень матрицы, возведя квадрат с использованием объектов матрицы Armadillo как в рекурсивном, так и итеративном порядке. Алгоритм можно найти здесь... https://en.wikipedia.org/wiki/Exponentiation_by_squaring. Мои функции были шаблонами, и я вычислил 1,000,000 12x12 матрицы, поднятые до мощности 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 с флагом С++ 11 (-std=c++11) и Armadillo 6.1 с Intel mkl. Компилятор Intel также показывает аналогичные результаты.

2
ответ дан 10 окт. '15 в 2:22
источник

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

См. ниже.

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

1
ответ дан 02 июня '17 в 14:14
источник

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

1
ответ дан 29 янв. '14 в 15:51
источник

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

int count(int i) {
  return i >= 100000000 ? i : count(i+1);
}
1
ответ дан 16 сент. '08 в 17:19
источник

Насколько я знаю, 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
ответ дан 10 нояб. '08 в 20:28
источник

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

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

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

0
ответ дан 21 апр. '13 в 9:30
источник

Используя только 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 запусков с использованием стандартного цикла

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

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

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

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

Рекурсия снова побеждает!

0
ответ дан 16 сент. '15 в 18:50
источник

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

Введем тип для простого дерева:

data Tree a = Branch (Tree a) (Tree a)
            | Leaf a
            deriving (Eq)

Мы можем прочитать это определение как "Дерево - это ветвь (которая содержит два дерева) или представляет собой лист (который содержит значение данных)". Таким образом, лист - это своего рода минимальный случай. Если дерево не является листом, то оно должно быть составным деревом, содержащим два дерева. Это единственные случаи.

Сделайте дерево:

example :: Tree Int
example = Branch (Leaf 1) 
                 (Branch (Leaf 2) 
                         (Leaf 3))

Теперь предположим, что мы хотим добавить 1 к каждому значению в дереве. Мы можем сделать это, позвонив:

addOne :: Tree Int -> Tree Int
addOne (Branch a b) = Branch (addOne a) (addOne b)
addOne (Leaf a)     = Leaf (a + 1)

Во-первых, обратите внимание, что это фактически рекурсивное определение. Он принимает конструкторы данных Branch и Leaf как случаи (и поскольку Leaf минимален, и это единственно возможные случаи), мы уверены, что функция завершится.

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

Кроме того, такую ​​рекурсию часто можно учесть в терминах "функтора". Мы можем сделать Деревья в Функторы, определив:

instance Functor Tree where fmap f (Leaf a)     = Leaf (f a)
                            fmap f (Branch a b) = Branch (fmap f a) (fmap f b)

и определяя:

addOne' = fmap (+1)

Мы можем отбросить другие схемы рекурсии, такие как катаморфизм (или сгиб) для типа алгебраических данных. Используя катаморфизм, мы можем написать:

addOne'' = cata go where
           go (Leaf a) = Leaf (a + 1)
           go (Branch a b) = Branch a b
-1
ответ дан 23 марта '13 в 2:36
источник

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

Рекурсия - это способ пойти, если вы хотите итерации через файлы, уверен, что как "найти" | "grep *" работает. Сразу две рекурсии, особенно с трубой (но не делайте кучу системных вызовов, как многие любят делать, если это все, что вы собираетесь использовать там, чтобы другие могли использовать).

Языки более высокого уровня и даже clang/cpp могут реализовывать его в фоновом режиме.

-2
ответ дан 30 нояб. '17 в 16:46
источник

Другие вопросы по меткам или Задайте вопрос