Я много использовал рекурсию в своих многолетних программах для решения простых задач, но я полностью осознаю, что иногда вам нужна итерация из-за проблем с памятью и скоростью.
Итак, когда-то в очень далеком прошлом я попытался найти, существует ли какой-либо «шаблон» или учебник, способ преобразования обычного рекурсивного подхода к итерации, и ничего не нашел. Или, по крайней мере, ничего, что я помню, это не помогло бы.
- Есть ли общие правила?
- Есть ли «шаблон»?
recursion
computer-science
theory
iteration
Густаво Каррено
источник
источник
Ответы:
Обычно я заменяю рекурсивный алгоритм итеративным алгоритмом, помещая параметры, которые обычно передаются рекурсивной функции в стек. Фактически, вы заменяете программный стек одним своим.
Примечание: если у вас более одного рекурсивного вызова внутри и вы хотите сохранить порядок вызовов, вы должны добавить их в обратном порядке в стек:
должен быть заменен
Изменить: статья Стеки и устранение рекурсии (или ссылка Резервное копирование статьи ) входит в более подробную информацию по этому вопросу.
источник
(node)->()
с ,(node)->[actions]
где действие() -> [actions]
. Затем снаружи вы просто выталкиваете действие / продолжение из стека, применяете / выполняете его, перемещаете действия, которые оно вернуло в стек, в обратном порядке и повторяете. Случайные / сложные обходы, вы просто фиксируете то, что было бы локальными переменными стека в указателях с подсчетом ссылок, которые вы закрываете в своих группах, тогда последующиеnew
мы можем создать объект в куче, а не в стеке. В отличие от стека, куча не имеет ограничений памяти. См. Gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.htmlДействительно, самый распространенный способ сделать это - сохранить свой собственный стек. Вот рекурсивная функция быстрой сортировки в C:
Вот как мы могли бы сделать его итеративным, сохранив свой собственный стек:
Очевидно, что этот пример не проверяет границы стека ... и на самом деле вы можете определить размер стека на основе наихудшего случая с учетом левого и правого значений. Но Вы получаете идею.
источник
O(N) = O(R*L)
, гдеL
есть сумма сложности "для слоя r", например, в этом случае у вас естьO(N)
работа на каждом шаге, делающем разбиения, рекурсивная глубинаO(R)
, то есть наихудший случайO(N)
, является средним случаемO(logN)
здесь.Кажется, никто не обращался к тому, где рекурсивная функция вызывает себя более одного раза в теле и обрабатывает возврат к определенной точке рекурсии (то есть не является примитивно-рекурсивной). Говорят, что каждая рекурсия может быть превращена в итерацию , поэтому кажется, что это должно быть возможно.
Я только что придумал пример C #, как это сделать. Предположим, у вас есть следующая рекурсивная функция, которая действует как обход по порядку и что AbcTreeNode - это 3-арное дерево с указателями a, b, c.
Итеративное решение:
источник
Стремитесь сделать рекурсивный вызов Tail Recursion (рекурсия, где последним оператором является рекурсивный вызов). Если у вас есть это, преобразовать его в итерацию, как правило, довольно легко.
источник
Ну, в общем, рекурсию можно имитировать как итерацию, просто используя переменную хранения. Обратите внимание, что рекурсия и итерация обычно эквивалентны; одно почти всегда может быть преобразовано в другое. Хвосто-рекурсивная функция очень легко преобразуется в итеративную. Просто сделайте переменную аккумулятора локальной, и выполняйте итерацию вместо recurse. Вот пример на C ++ (если бы не использование аргумента по умолчанию):
Зная меня, я, вероятно, допустил ошибку в коде, но идея есть.
источник
Даже использование стека не преобразует рекурсивный алгоритм в итеративный. Обычная рекурсия - это рекурсия, основанная на функциях, и если мы используем стек, то она становится рекурсией, основанной на стеке. Но это все еще рекурсия.
Для рекурсивных алгоритмов сложность пространства равна O (N), а сложность времени - O (N). Для итерационных алгоритмов сложность пространства равна O (1), а сложность времени равна O (N).
Но если мы используем стек вещей с точки зрения сложности остается тем же. Я думаю, что только хвостовая рекурсия может быть преобразована в итерацию.
источник
copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];
пространства памяти, а сложность времени - O (N) в зависимости от размера данных, но это явно итерационный алгоритм.В статье, посвященной удалению стеков и рекурсии, отражена идея экстернализации кадра стека в куче, но не предоставляется простой и воспроизводимый способ преобразования. Ниже один.
При преобразовании в итеративный код необходимо учитывать, что рекурсивный вызов может происходить из произвольно глубокого блока кода. Это не только параметры, но и точка возврата к логике, которую еще предстоит выполнить, и состояние переменных, которые участвуют в последующих условных выражениях, которые имеют значение. Ниже приведен очень простой способ преобразования в итеративный код с наименьшими изменениями.
Рассмотрим этот рекурсивный код:
Итерационный код:
Обратите внимание, что структура кода все еще остается верной рекурсивной логике, а модификации минимальны, что приводит к меньшему количеству ошибок. Для сравнения я пометил изменения с ++ и -. Большинство новых вставленных блоков, кроме v.push_back, являются общими для любой преобразованной итерационной логики.
+++++++++++++++++++++++++
------------------------
+++++++++++++++++++++++++
-------------------------
+++++++++++++++++++++++++
-------------------------
+++++++++++++++++++++++++
-------------------------
источник
stackitem
объекты выделяются со значением мусора дляra
. Все по-прежнему работает в наиболее похожем случае, но еслиra
по совпадению будет 1 или 2, вы получите неправильное поведение. Решение состоит в том, чтобы инициализироватьra
до 0.stackitem
нельзя нажимать без инициализации. Но да, инициализация в 0 будет ловить ошибки.v.pop_back()
вместо обоих адресов возврата не указано выражение?Поиском в Google "Стиль продолжения продолжения". Существует общая процедура для преобразования в хвостовой рекурсивный стиль; Существует также общая процедура преобразования хвостовых рекурсивных функций в циклы.
источник
Просто убивает время ... Рекурсивная функция
может быть преобразован в
источник
Обычно метод, позволяющий избежать переполнения стека для рекурсивных функций, называется методом батута, который широко применяется разработчиками Java.
Однако, для C # есть небольшой вспомогательный метод здесь , что превращает вашу рекурсивную функцию итерационного , не требуя к логике изменения или сделать код-понятным. C # - такой приятный язык, что с ним можно создавать удивительные вещи.
Он работает, оборачивая части метода вспомогательным методом. Например, следующая рекурсивная функция:
Превращается в:
источник
Думая о вещах, которые действительно нуждаются в стеке:
Если мы рассмотрим шаблон рекурсии как:
Например, классическая Ханойская башня
Это может быть преобразовано в цикл, работающий с явным стеком, путем его преобразования:
Для Ханойской Башни это становится:
Здесь есть большая гибкость в отношении того, как вы определяете свой стек. Вы можете сделать свой стек списком
Command
объектов, которые делают сложные вещи. Или вы можете пойти в противоположном направлении и составить список простых типов (например, «задание» может быть 4 элемента в стекеint
, а не один элемент в стекеTask
).Все это означает, что память для стека находится в куче, а не в стеке выполнения Java, но это может быть полезно, поскольку у вас больше контроля над ней.
источник
Один шаблон для поиска - это рекурсивный вызов в конце функции (так называемая хвостовая рекурсия). Это может быть легко заменено на некоторое время. Например, функция foo:
заканчивается звонком в foo. Это можно заменить на:
который устраняет второй рекурсивный вызов.
источник
Вопрос , который был закрыт как дубликат этой имел очень специфическую структуру данных:
Узел имел следующую структуру:
Функция рекурсивного удаления выглядела так:
В общем, не всегда возможно избежать стека для рекурсивных функций, которые вызывают себя более одного раза (или даже один раз). Однако для этой конкретной структуры это возможно. Идея состоит в том, чтобы объединить все узлы в один список. Это достигается путем помещения текущего узла
child
в конец списка верхней строки.Этот метод может быть применен к любой связанной с данными структуре, которая может быть преобразована в группу доступности базы данных с детерминированным топологическим порядком. Текущие дочерние узлы переставляются так, что последний дочерний элемент принимает всех остальных дочерних элементов. Затем текущий узел может быть удален, и обход может затем перейти к оставшемуся дочернему элементу.
источник
Рекурсия - это не что иное, как процесс вызова одной функции из другой, только этот процесс выполняется путем вызова самой функции. Как мы знаем, когда одна функция вызывает другую функцию, первая функция сохраняет свое состояние (свои переменные), а затем передает управление вызываемой функции. Вызываемая функция может быть вызвана с использованием одного и того же имени переменных. Ex fun1 (a) может вызвать fun2 (a). Когда мы делаем рекурсивный вызов, ничего нового не происходит. Одна функция вызывает себя, передавая один и тот же тип и похожие по имени переменные (но, очевидно, значения, хранящиеся в переменных, различаются, только имя остается тем же самым). Но перед каждым вызовом функция сохраняет свое состояние, и этот процесс сохранения продолжается. ЭКОНОМИЯ СДЕЛАНА НА СТЕКЕ.
СЕЙЧАС СТЕК ВХОДИТ В ИГРА.
Поэтому, если вы пишете итеративную программу и каждый раз сохраняете состояние в стеке, а затем извлекаете значения из стека, когда это необходимо, вы успешно преобразовали рекурсивную программу в итеративную!
Доказательство простое и аналитическое.
В рекурсии компьютер поддерживает стек, а в итерационной версии вам придется вручную поддерживать стек.
Подумайте об этом, просто преобразуйте рекурсивную программу поиска в глубину (на графиках) в итерационную программу dfs.
Всего наилучшего!
источник
Еще один простой и полный пример превращения рекурсивной функции в итеративную с использованием стека.
источник
Грубое описание того, как система берет любую рекурсивную функцию и выполняет ее, используя стек:
Это призвано показать идею без подробностей. Рассмотрим эту функцию, которая распечатывает узлы графа:
Например, график: A-> B A-> C show (A) выведет B, A, C
Вызов функции означает сохранение локального состояния и точки продолжения, чтобы вы могли вернуться и затем перейти к функции, которую вы хотите вызвать.
Например, предположим, что шоу (A) начинает работать. Вызов функции в строке 3. show (B) означает - Добавить элемент в стек, что означает «вам нужно продолжить со строки 2 с локальной переменной state node = A» - Перейти к строке 0 с node = B.
Чтобы выполнить код, система выполняет инструкции. Когда происходит вызов функции, система отправляет информацию, необходимую для возврата туда, где она была, запускает код функции, а когда функция завершается, выдает информацию о том, куда ей нужно перейти, чтобы продолжить.
источник
Эта ссылка дает некоторое объяснение и предлагает идею сохранения «местоположения», чтобы иметь возможность добраться до точного места между несколькими рекурсивными вызовами:
Однако все эти примеры описывают сценарии, в которых рекурсивный вызов выполняется фиксированное количество раз. Все становится сложнее, когда у вас есть что-то вроде:
источник
Существует общий способ преобразования рекурсивного обхода в итератор с помощью ленивого итератора, который объединяет нескольких поставщиков итераторов (лямбда-выражение, которое возвращает итератор). Смотрите мой Преобразование рекурсивного обхода в итератор .
источник
Мои примеры в Clojure, но их должно быть довольно легко перевести на любой язык.
Учитывая эту функцию, которая
StackOverflow
s для больших значений n:мы можем определить версию, которая использует свой собственный стек следующим образом:
где
return
определяется как:Это работает и для более сложных функций, например, функции ackermann :
может быть преобразован в:
источник