Есть ли снижение производительности, если мы используем цикл вместо рекурсии или наоборот в алгоритмах, где оба могут служить одной и той же цели? Например: проверьте, является ли данная строка палиндромом. Я видел много программистов, использующих рекурсию как способ показать себя, когда простой итерационный алгоритм может соответствовать всем требованиям. Играет ли компилятор жизненно важную роль при принятии решения, что использовать?
performance
algorithm
language-agnostic
recursion
всемогущий
источник
источник
Ответы:
Возможно, что рекурсия будет более дорогой, в зависимости от того, является ли рекурсивная функция хвостовой рекурсивной (последняя строка - рекурсивный вызов). Хвостовая рекурсия должна распознаваться компилятором и оптимизироваться для его итеративного аналога (при сохранении краткой и ясной реализации, которую вы имеете в своем коде).
Я бы написал алгоритм таким образом, чтобы он был наиболее понятным и понятным для бедного лоха (будь то вы или кто-то еще), который должен поддерживать код через несколько месяцев или лет. Если вы столкнетесь с проблемами производительности, то профилируйте свой код, а затем и только потом изучите оптимизацию, перейдя к итеративной реализации. Возможно, вы захотите изучить запоминание и динамическое программирование .
источник
tail recursion is optimized by compilers
Но не все компиляторы поддерживают хвостовую рекурсию.Циклы могут повысить производительность вашей программы. Рекурсия может повысить производительность вашего программиста. Выберите, что важнее в вашей ситуации!
источник
Сравнение рекурсии с итерацией аналогично сравнению отвертки с крестообразной головкой и отвертки с плоской головкой. По большей части вы можете удалить любой винт с крестообразным шлицем с плоской головкой, но было бы проще, если бы вы использовали отвертку, предназначенную для этого винта, верно?
Некоторые алгоритмы просто поддаются рекурсии из-за того, как они спроектированы (последовательности Фибоначчи, обход древовидной структуры и т. Д.). Рекурсия делает алгоритм более лаконичным и более простым для понимания (поэтому может использоваться и использоваться повторно).
Кроме того, некоторые рекурсивные алгоритмы используют «Ленивая оценка», что делает их более эффективными, чем их итеративные братья. Это означает, что они выполняют дорогостоящие вычисления только в то время, когда они необходимы, а не при каждом запуске цикла.
Этого должно быть достаточно, чтобы вы начали. Я выкопаю несколько статей и примеров для вас тоже.
Ссылка 1: Haskel против PHP (рекурсия против итерации)
Вот пример, где программист должен был обрабатывать большой набор данных с использованием PHP. Он показывает, как легко было бы иметь дело в Haskel с помощью рекурсии, но поскольку у PHP не было простого способа реализовать тот же метод, он был вынужден использовать итерацию для получения результата.
Ссылка 2: Освоение рекурсии
Большая часть плохой репутации рекурсии происходит из-за высокой стоимости и неэффективности в императивных языках. Автор этой статьи рассказывает о том, как оптимизировать рекурсивные алгоритмы, чтобы сделать их быстрее и эффективнее. Он также рассказывает о том, как преобразовать традиционный цикл в рекурсивную функцию, и о преимуществах использования хвостовой рекурсии. Его заключительные слова действительно суммировали некоторые из моих ключевых моментов, я думаю:
Ссылка 3: Является ли рекурсия быстрее, чем зацикливание? (Ответ)
Вот ссылка на ответ на вопрос stackoverflow, который похож на ваш. Автор указывает, что многие тесты, связанные либо с рекурсивными, либо с циклическими, очень специфичны для каждого языка. Императивные языки обычно быстрее с использованием цикла и медленнее с рекурсией и наоборот для функциональных языков. Я полагаю, что основной смысл этой ссылки заключается в том, что очень трудно ответить на вопрос в не зависящем от языка / ситуации слепом смысле.
источник
Рекурсия обходится дороже в памяти, так как каждый рекурсивный вызов обычно требует, чтобы адрес памяти был помещен в стек, чтобы впоследствии программа могла вернуться к этой точке.
Тем не менее, во многих случаях рекурсия намного более естественна и удобочитаема, чем циклы - как при работе с деревьями. В этих случаях я бы рекомендовал придерживаться рекурсии.
источник
Как правило, можно ожидать ухудшения производительности в другом направлении. Рекурсивные вызовы могут привести к созданию дополнительных кадров стека; штраф за это варьируется. Кроме того, в некоторых языках, таких как Python (точнее, в некоторых реализациях некоторых языков ...), вы можете довольно легко работать с ограничениями стека для задач, которые вы можете указать рекурсивно, например, для поиска максимального значения в древовидной структуре данных. В этих случаях вы действительно хотите придерживаться петель.
Написание хороших рекурсивных функций может несколько снизить потери производительности, при условии, что у вас есть компилятор, который оптимизирует хвостовые рекурсии и т. Д. (Также дважды проверьте, чтобы убедиться, что функция действительно является хвостовой рекурсивной - это одна из тех ошибок, которые многие люди допускают ошибки на.)
Помимо «крайних» случаев (высокопроизводительные вычисления, очень большая глубина рекурсии и т. Д.), Предпочтительно использовать подход, который наиболее четко выражает ваши намерения, хорошо спроектирован и удобен в обслуживании. Оптимизировать только после выявления необходимости.
источник
Рекурсия лучше, чем итерация, для задач, которые можно разбить на несколько меньших частей.
Например, чтобы создать рекурсивный алгоритм Фибоначчи, вы разбиваете 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) рассчитывается дважды!
Лучшим примером является рекурсивный алгоритм для дерева. Задача анализа родительского узла может быть разбита на несколько меньших задач анализа каждого дочернего узла. В отличие от примера Фибоначчи, меньшие проблемы не зависят друг от друга.
Так что да - рекурсия лучше, чем итерация, для задач, которые можно разбить на несколько меньших, независимых, похожих задач.
источник
Ваша производительность ухудшается при использовании рекурсии, потому что вызов метода на любом языке требует большой подготовки: вызывающий код отправляет адрес возврата, параметры вызова, некоторая другая контекстная информация, такая как регистры процессора, может быть где-то сохранена, а во время возврата Вызванный метод отправляет возвращаемое значение, которое затем извлекается вызывающей стороной, и любая ранее сохраненная контекстная информация будет восстановлена. разница в производительности между итеративным и рекурсивным подходом заключается во времени, которое занимают эти операции.
С точки зрения реализации вы действительно начинаете замечать разницу, когда время, необходимое для обработки вызывающего контекста, сопоставимо со временем, которое требуется для выполнения вашего метода. Если ваш рекурсивный метод выполняется дольше, чем вызывающая часть управления контекстом, идите рекурсивным путем, поскольку код, как правило, более читабелен и прост для понимания, и вы не заметите потери производительности. В противном случае идите итеративно по причинам эффективности.
источник
Я считаю, что рекурсия хвоста в Java в настоящее время не оптимизирована. Подробности пронизывают это обсуждение LTU и связанные с ними связями. Это может быть функция в следующей версии 7, но, очевидно, она представляет определенные трудности в сочетании с проверкой стека, поскольку некоторые кадры будут отсутствовать. Проверка стека использовалась для реализации их детальной модели безопасности начиная с Java 2.
http://lambda-the-ultimate.org/node/1333
источник
Во многих случаях он дает гораздо более элегантное решение по сравнению с итеративным методом, распространенным примером является обход двоичного дерева, поэтому его не обязательно сложнее поддерживать. В общем, итеративные версии обычно немного быстрее (и во время оптимизации вполне могут заменить рекурсивную версию), но рекурсивные версии проще понять и правильно реализовать.
источник
Рекурсия очень полезна в некоторых ситуациях. Например, рассмотрим код для поиска факториала
Теперь рассмотрим это с помощью рекурсивной функции
Наблюдая за этими двумя, мы видим, что рекурсию легко понять. Но если он не используется с осторожностью, он также может быть очень подвержен ошибкам. Предположим, что если мы пропустим
if (input == 0)
, то код будет выполняться некоторое время и обычно заканчивается переполнением стека.источник
foldl (*) 1 [1..n]
вот и все.Во многих случаях рекурсия происходит быстрее из-за кэширования, что повышает производительность. Например, вот итерационная версия сортировки слиянием с использованием традиционной процедуры слияния. Он будет работать медленнее, чем рекурсивная реализация из-за улучшенной производительности кэширования.
Итеративная реализация
Рекурсивная реализация
PS - это то, что рассказал профессор Кевин Уэйн (Принстонский университет) на курсе по алгоритмам, представленным на Coursera.
источник
Используя рекурсию, вы несете стоимость вызова функции с каждой «итерацией», в то время как с циклом единственное, что вы обычно платите, это увеличение / уменьшение. Итак, если код для цикла не намного сложнее, чем код для рекурсивного решения, цикл обычно будет лучше, чем рекурсия.
источник
Рекурсия и итерация зависит от бизнес-логики, которую вы хотите реализовать, хотя в большинстве случаев она может использоваться взаимозаменяемо. Большинство разработчиков идут на рекурсию, потому что это легче понять.
источник
Это зависит от языка. В Java вы должны использовать циклы. Функциональные языки оптимизируют рекурсию.
источник
Если вы просто перебираете список, то, конечно же, перебирайте.
В нескольких других ответах упоминается (в первую очередь) обход дерева. Это действительно отличный пример, потому что это очень распространенная вещь, которую нужно делать с очень распространенной структурой данных. Рекурсия чрезвычайно интуитивно понятна для этой проблемы.
Проверьте методы поиска здесь: http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html
источник
Рекурсия является более простой (и, следовательно, более фундаментальной), чем любое возможное определение итерации. Вы можете определить полную по Тьюрингу систему только с парой комбинаторов (да, даже сама рекурсия является производным понятием в такой системе). Лямбда- исчисление - это не менее мощная фундаментальная система с рекурсивными функциями. Но если вы хотите правильно определить итерацию, для начала вам понадобится гораздо больше примитивов.
Что касается кода - нет, рекурсивный код на самом деле гораздо легче понять и поддерживать, чем чисто итеративный, поскольку большинство структур данных являются рекурсивными. Конечно, чтобы сделать это правильно, нужен язык с поддержкой функций и замыканий высшего порядка, по крайней мере - для аккуратного получения всех стандартных комбинаторов и итераторов. В C ++, конечно, сложные рекурсивные решения могут выглядеть немного безобразно, если только вы не хардкорный пользователь FC ++ и тому подобное.
источник
Я бы подумал, что в (не хвостовой) рекурсии будет падение производительности при выделении нового стека и т. Д. Каждый раз, когда вызывается функция (конечно, в зависимости от языка).
источник
это зависит от "глубины рекурсии". это зависит от того, насколько накладные расходы на вызов функции будут влиять на общее время выполнения.
Например, вычисление классического факториала рекурсивным способом очень неэффективно из-за: - риска переполнения данных - риска переполнения стека - издержки вызова функций занимают 80% времени выполнения
при разработке алгоритма min-max для анализа позиции в игре в шахматы, который будет анализировать последующие N ходов, можно реализовать рекурсию по «глубине анализа» (как я делаю ^ _ ^)
источник
Рекурсия? С чего начать, вики расскажет вам «это процесс повторения предметов самоподобным способом»
В те времена, когда я занимался Си, рекурсия на С ++ была хорошей идеей, наподобие «Хвостовой рекурсии». Вы также найдете много алгоритмов сортировки, использующих рекурсию. Пример быстрой сортировки: http://alienryderflex.com/quicksort/
Рекурсия подобна любому другому алгоритму, полезному для конкретной задачи. Возможно, вы не сможете найти применение сразу или часто, но возникнет проблема, вы будете рады, что она доступна.
источник
В C ++, если рекурсивная функция является шаблонной, тогда у компилятора больше шансов оптимизировать ее, так как все дедукции типов и реализации функций будут происходить во время компиляции. Современные компиляторы также могут встроить функцию, если это возможно. Таким образом, если использовать флаги оптимизации, такие как
-O3
или-O2
ing++
, то рекурсии могут иметь шанс быть быстрее, чем итерации. В итерационных кодах компилятор получает меньше возможностей для его оптимизации, поскольку он уже находится в более или менее оптимальном состоянии (если он написан достаточно хорошо).В моем случае я пытался реализовать возведение в матрицу путем возведения в квадрат с использованием матричных объектов Armadillo как рекурсивным, так и итерационным способом. Алгоритм можно найти здесь ... https://en.wikipedia.org/wiki/Exponentiation_by_squaring . Мои функции были шаблонными, и я рассчитал
1,000,000
12x12
матрицы, возведенные в степень10
. Я получил следующий результат:Эти результаты были получены с использованием gcc-4.8 с c ++ 11 flag (
-std=c++11
) и Armadillo 6.1 с Intel mkl. Компилятор Intel также показывает аналогичные результаты.источник
Майк прав. Хвостовая рекурсия не оптимизируется компилятором Java или JVM. Вы всегда получите переполнение стека чем-то вроде этого:
источник
Вы должны иметь в виду, что при использовании слишком глубокой рекурсии вы столкнетесь с переполнением стека, в зависимости от допустимого размера стека. Чтобы предотвратить это, обязательно предоставьте базовый вариант, который завершает вашу рекурсию.
источник
Недостатком рекурсии является то, что алгоритм, который вы пишете с использованием рекурсии, имеет O (n) пространственную сложность. Хотя итеративный подход имеет пространственную сложность O (1). Это преимущество использования итерации над рекурсией. Тогда почему мы используем рекурсию?
Увидеть ниже.
Иногда проще написать алгоритм с использованием рекурсии, в то время как немного сложнее написать тот же алгоритм с использованием итерации. В этом случае, если вы решите следовать итерационному подходу, вам придется обрабатывать стек самостоятельно.
источник
Если итерации атомарны и на порядки дороже, чем проталкивание нового стекового фрейма и создание нового потока, и у вас есть несколько ядер, и ваша среда выполнения может использовать все из них, то рекурсивный подход может привести к значительному повышению производительности в сочетании с многопоточности. Если среднее число итераций непредсказуемо, то было бы неплохо использовать пул потоков, который будет контролировать распределение потоков и не позволит вашему процессу создавать слишком много потоков и перегружать систему.
Например, в некоторых языках существуют рекурсивные многопоточные реализации сортировки слиянием.
Но опять же, многопоточность может использоваться с циклическим, а не с рекурсивным, поэтому то, насколько хорошо эта комбинация будет работать, зависит от большего количества факторов, включая ОС и механизм распределения потоков.
источник
Насколько я знаю, Perl не оптимизирует хвостовые рекурсивные вызовы, но вы можете это подделать.
При первом вызове он выделит место в стеке. Затем он изменит свои аргументы и перезапустит подпрограмму, не добавляя ничего в стек. Поэтому он сделает вид, что никогда не называл себя, превращая его в итеративный процесс.
Обратите внимание, что нет "
my @_;
" или "local @_;
", если вы сделали это, больше не будет работать.источник
Используя только Chrome 45.0.2454.85 м, рекурсия кажется намного быстрее.
Вот код:
ПОЛУЧЕННЫЕ РЕЗУЛЬТАТЫ
// 100 прогонов, используя стандартный цикл for
100x для цикла. Время для завершения: 7.683мс
// 100 прогонов с использованием функционально-рекурсивного подхода с хвостовой рекурсией
100-кратный рекурсивный прогон. Время для завершения: 4.841мс
На скриншоте ниже, рекурсия побеждает снова с большим отрывом, когда выполняется при 300 циклах на тест
источник
Я обнаружил еще одно различие между этими подходами. Это выглядит просто и неважно, но оно играет очень важную роль, пока вы готовитесь к собеседованиям, и эта тема возникает, так что присмотритесь.
Вкратце: 1) итеративный обход по порядку не прост - это делает DFT более сложным 2) проверка циклов легче с помощью рекурсии
Подробности:
В рекурсивном случае легко создать обходы до и после:
Представьте себе довольно стандартный вопрос: «напечатать все задачи, которые должны быть выполнены для выполнения задачи 5, когда задачи зависят от других задач»
Пример:
Обратите внимание, что рекурсивный обход после заказа не требует последующего обращения результата. Дети напечатали сначала, а ваше задание в вопросе напечатано последним. Все хорошо. Вы можете выполнить рекурсивный обход по предварительному заказу (также показан выше), и для этого потребуется изменить список результатов.
Не так просто с итеративным подходом! При итеративном подходе (один стек) вы можете выполнять только предварительный порядок обхода, поэтому в конце вы должны обратить массив результатов:
Выглядит просто, нет?
Но это ловушка в некоторых интервью.
Это означает следующее: при рекурсивном подходе вы можете внедрить метод Depth First Traversal, а затем выбрать, какой порядок вам нужен до или после публикации (просто изменив местоположение «print», в нашем случае «добавление в список результатов»). ). С помощью итеративного подхода (один стек) вы можете легко выполнять только обход по предварительному заказу, и поэтому в ситуации, когда дочерние элементы должны быть напечатаны первыми (почти во всех ситуациях, когда вам нужно начать печать с нижних узлов, идущих вверх), вы находитесь в проблема. Если у вас возникли проблемы, вы можете вернуться позже, но это будет дополнение к вашему алгоритму. И если интервьюер смотрит на часы, это может стать для вас проблемой. Существуют сложные способы итеративного обхода после заказа, они существуют, но они не просты . Пример:https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/
Таким образом, суть: я бы использовал рекурсию во время собеседований, проще управлять и объяснять. В любом неотложном случае у вас есть простой путь от обхода до заказа к отправке. С итеративным вы не так гибки.
Я бы использовал рекурсию и затем сказал бы: «Хорошо, но итеративный может предоставить мне более прямой контроль над используемой памятью, я могу легко измерить размер стека и предотвратить некоторые опасные переполнения ...»
Еще один плюс рекурсии - проще избегать / замечать циклы в графе.
Пример (преудокод):
источник
Может быть интересно написать это как рекурсию или как практику.
Однако, если код будет использоваться в производстве, вам необходимо учитывать возможность переполнения стека.
Оптимизация хвостовой рекурсии может устранить переполнение стека, но вы хотите решить эту проблему, и вам нужно знать, что вы можете рассчитывать на ее оптимизацию в вашей среде.
Каждый раз, когда алгоритм повторяется, насколько размер данных
n
уменьшается или уменьшается?Если вы уменьшаете размер данных или
n
вдвое каждый раз, когда выполняете рекурсию, то в общем случае вам не нужно беспокоиться о переполнении стека. Скажем, если для переполнения стека программа должна быть глубиной 4000 или глубиной 10000, размер вашей программы должен быть примерно 2 000, чтобы переполнение стека вашей программой. Для сравнения: в последнее время самое большое устройство хранения может содержать 2 61 байт, и если у вас есть 2 61 таких устройств, вы имеете дело только с 2 122 размерами данных. Если вы смотрите на все атомы во вселенной, то, по оценкам, это может быть менее 2 84, Если вам нужно иметь дело со всеми данными во вселенной и их состояниями за каждую миллисекунду с момента рождения вселенной, которая оценивается в 14 миллиардов лет назад, это может быть только 2 153 . Таким образом, если ваша программа может обрабатывать 2 000 единиц данных илиn
вы можете обрабатывать все данные в юниверсе, и программа не будет переполняться стеком. Если вам не нужно иметь дело с числами размером до 2 000 (4000-битное целое число), то в общем случае вам не нужно беспокоиться о переполнении стека.Однако, если вы уменьшаете размер данных или
n
на постоянную величину каждый раз, когда выполняете рекурсию, то вы можете столкнуться с переполнением стека, когда ваша программа работает хорошо, когдаn
,1000
но в какой-то ситуации, когдаn
становится просто20000
.Так что если у вас есть возможность переполнения стека, попробуйте сделать это итеративным решением.
источник
Я собираюсь ответить на ваш вопрос, спроектировав структуру данных на Haskell с помощью «индукции», которая является своего рода «двойственной» рекурсии. И тогда я покажу, как эта двойственность приводит к хорошим вещам.
Введем тип для простого дерева:
Мы можем прочитать это определение как «Дерево - это ветвь (которая содержит два дерева) или лист (который содержит значение данных)». Таким образом, лист является своего рода минимальным случаем. Если дерево не является листом, то это должно быть составное дерево, содержащее два дерева. Это единственные случаи.
Давайте сделаем дерево:
Теперь предположим, что мы хотим добавить 1 к каждому значению в дереве. Мы можем сделать это, позвонив:
Во-первых, обратите внимание, что это на самом деле рекурсивное определение. В качестве случаев используются конструкторы данных Branch и Leaf (а так как Leaf минимален и это единственно возможные случаи), мы уверены, что функция завершится.
Что нужно, чтобы написать addOne в итеративном стиле? Как будет выглядеть цикл в произвольном количестве ветвей?
Кроме того, этот тип рекурсии часто может быть учтен в терминах «функтора». Мы можем превратить деревья в функторы, определив:
и определяя:
Мы можем выделить другие схемы рекурсии, такие как катаморфизм (или складывание) для алгебраического типа данных. Используя катаморфизм, мы можем написать:
источник
Переполнение стека произойдет, только если вы программируете на языке, который не имеет встроенного управления памятью ... В противном случае, убедитесь, что у вас есть что-то в вашей функции (или вызов функции, STDLbs и т. Д.). Без рекурсии было бы просто невозможно иметь такие вещи, как ... Google или SQL, или любое другое место, где нужно эффективно сортировать большие структуры данных (классы) или базы данных.
Рекурсия - это то, что нужно, если вы хотите перебирать файлы, почти наверняка вот так: 'find * | ? grep * 'работает. Вроде бы двойная рекурсия, особенно с конвейером (но не делайте кучу системных вызовов, как многие любят делать, если вы собираетесь что-то делать для других).
Языки более высокого уровня и даже clang / cpp могут реализовать то же самое в фоновом режиме.
источник