Может ли каждая рекурсия быть преобразована в итерацию?

на reddit thread поднял, по-видимому, интересный вопрос:

хвост рекурсивные функции могут быть тривиально преобразованы в итерационные функции. Другие могут быть преобразованы с помощью явного стека. Может каждый рекурсия будет преобразована в итерацию?

(контр?) примером в посте является пара:

(define (num-ways x y)
  (case ((= x 0) 1)
        ((= y 0) 1)
        (num-ways2 x y) ))

(define (num-ways2 x y)
  (+ (num-ways (- x 1) y)
     (num-ways x (- y 1))

18 ответов


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

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

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

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

Я вижу одну вескую причину. Предположим, у вас есть прототип системы на языке сверхвысокого уровня, например [надевание асбестового нижнего белья] схема, Lisp, Haskell, OCaml, Perl или Паскаль. Предположим, условия таковы, что вам нужна реализация на C или Java. (Возможно, это политика.) Тогда вы, конечно, могли бы иметь некоторые функции, написанные рекурсивно, но которые, переведенные буквально, взорвали бы вашу систему выполнения. Например, бесконечная хвостовая рекурсия возможна в схеме, но та же идиома вызывает проблему для существующих сред C. Другим примером является использование лексически вложенных функций и статической области, которую поддерживает Pascal, но не C.

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


всегда ли можно написать нерекурсивную форму для каждой рекурсивной функции?

да. Простое формальное доказательство-показать, что оба µ рекурсии и нерекурсивное исчисление, такое как GOTO, являются полными Тьюринга. Поскольку все полные исчисления Тьюринга строго эквивалентны по своей выразительной силе, все рекурсивные функции могут быть реализованы нерекурсивным полным исчислением Тьюринга.

к сожалению, я не удалось найти хорошее формальное определение GOTO online, поэтому вот одно:

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

  • HALT, который останавливает выполнение
  • r = r + 1 здесь r - любой регистр
  • r = r – 1 здесь r - любой регистр
  • GOTO x здесь x метка
  • IF r ≠ 0 GOTO x где r любой регистр и x метка
  • метка, за которой следует любая из вышеперечисленных команд.

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


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


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

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


Да, используя явно стек (но рекурсия гораздо приятнее читать, IMHO).


  • поток выполнения рекурсивной функции может быть представлен в виде дерева.

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

  • глубина-первый обход можно сделать с помощью стека, ширина-первый обход можно сделать с помощью очереди.

Итак, ответ: да. Почему: https://stackoverflow.com/a/531721/2128327.

можно ли выполнить рекурсию в одном цикле? Да, потому что

машина Тьюринга делает все, что она делает, выполняя один цикл:

  1. получить инструкции,
  2. оценить его,
  3. Гото 1.

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


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

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


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

Это меньше проблем сейчас же мода сместилась в сторону других технологий.


все вычислимые функции могут быть вычислены машинами Тьюринга, и, следовательно, рекурсивные системы и машины Тьюринга (итерационные системы) эквивалентны.


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

приведенные ниже случаи относятся к числу простых:


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

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

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

http://en.wikipedia.org/wiki/Trampoline_ (компьютеры)


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


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

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

решение рекуррентного отношения означает получение решение закрытой формы: нерекурсивный функция n.

обратите внимание на последний абзац запись.


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

Подробнее: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions


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

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

кстати, существуют языки Тьюринга, которые реализуют рекурсию (например, SML) только как средство цикла. Существуют также языки Тьюринга, которые реализуют итерацию только как средство цикла (например, FORTRAN IV). Тезис Черча-Тьюринга доказывает, что все возможное в рекурсия-только языки могут быть выполнены на нерекурсивном языке и наоборот тем, что они оба имеют свойство Тьюринга-полноты.


вот итеративный алгоритм:

def howmany(x,y)
  a = {}
  for n in (0..x+y)
    for m in (0..n)
      a[[m,n-m]] = if m==0 or n-m==0 then 1 else a[[m-1,n-m]] + a[[m,n-m-1]] end
    end
  end
  return a[[x,y]]
end

вопрос: если в качестве первой вещи функция делает копию себя в случайном пустом пространстве памяти, а затем вместо вызова себя вызывает копию, это все еще рекурсия?(1) я бы сказал Да.

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

более того, я не вижу, как CT демонстрирует, что все рекурсивные алгоритмы могут стать итеративными. Мне только кажется, что "все", имеющее "силу" машины Тьюринга, может выразить все алгоритмы, которые это может выразить. если машина Тьюринга не может рекурсии, мы уверены, что каждый рекурсивный algo имеет свой интерактивный перевод... Машина Тьюринга может рекурсии? По моему мнению, если это может быть "реализовано" (by любое значение), тогда мы можем сказать, что оно есть. Так ли это? Я не знаю.

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

избегая копирования(1) и "имитированного стека"(2), мы продемонстрировали, что каждый рекурсивное algo можно, на реальных машинах, выражать итеративно?! Я не вижу, где мы это продемонстрировали.