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

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

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

28 ответов

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

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

159
ответ дан Paul Osborne 16 сент. '08 в 16:52
источник поделиться

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

250
ответ дан Leigh Caldwell 16 сент. '08 в 17:11
источник поделиться

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

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

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

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

Ссылка 1: Haskel vs 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, который похож на ваш. Автор отмечает, что многие тесты, связанные с рекурсивным или циклическим циклом, - это очень язык. Императивные языки, как правило, быстрее используют цикл и медленнее с рекурсией и наоборот для функциональных языков. Я предполагаю, что основной смысл, который следует извлечь из этой ссылки, состоит в том, что очень трудно ответить на вопрос в языковом агностическом/ситуационном слепом смысле.

Рекурсия быстрее, чем цикл?

72
ответ дан Swift 24 июня '11 в 6:47
источник поделиться

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

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

16
ответ дан Doron Yaacoby 16 сент. '08 в 16:43
источник поделиться

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

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

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

11
ответ дан zweiterlinde 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
ответ дан Ben 24 июня '11 в 7:18
источник поделиться

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

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

7
ответ дан entzik 16 сент. '08 в 16:55
источник поделиться

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

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

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

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

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

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

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

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

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

4
ответ дан MovEaxEsp 16 сент. '08 в 16:38
источник поделиться

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

4
ответ дан mc 16 сент. '08 в 16:35
источник поделиться

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

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

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

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

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

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

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

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

3
ответ дан SK-logic 24 июня '11 в 13:33
источник поделиться

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

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

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

2
ответ дан ugasoft 16 сент. '08 в 16:48
источник поделиться

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

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

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

2
ответ дан Nickz 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
ответ дан Titas Chanda 10 окт. '15 в 2:22
источник поделиться

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

2
ответ дан metadave 16 сент. '08 в 16:39
источник поделиться

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

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

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

См. ниже.

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

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

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

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

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

Я собираюсь ответить на ваш вопрос, создав структуру данных 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
0
ответ дан nomen 23 марта '13 в 2:36
источник поделиться

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

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

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

0
ответ дан ccpizza 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
ответ дан Alpha G33k 16 сент. '15 в 18:50
источник поделиться

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

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

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

-1
ответ дан F13n0 30 нояб. '17 в 16:46
источник поделиться

Другие вопросы по меткам