Чи може кожна рекурсія перетворитися на ітерацію?


181

Reddit нитка виховав , мабуть , цікаве питання:

Рекурсивні функції хвоста можуть тривіально перетворюватися на ітеративні функції. Інші, можна перетворити, використовуючи явний стек. Чи може кожна рекурсія перетворитися на ітерацію?

Прикладом (лічильником?) В публікації є пара:

(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))

3
Я не бачу, як це зустрічний приклад. Техніка стеку буде працювати. Це буде не дуже, і я не збираюся це писати, але це можливо. Здається, akdas підтверджує це у вашому посиланні.
Метью Флашен

Ваш (num-way xy) - це просто (x + y) choosex = (x + y)! / (X! Y!), Якому не потрібна рекурсія.
ShreevatsaR


Я б сказав, що рекурсія - це лише зручність.
e2-e4

Відповіді:


181

Чи завжди ви можете перетворити рекурсивну функцію в ітеративну? Так, абсолютно, і теза Церкви Тьюрінга доводить це, якщо служить пам'ять. Простіше кажучи, він зазначає, що те, що можна обчислити рекурсивними функціями, обчислюється за допомогою ітеративної моделі (наприклад, машини Тьюрінга) і навпаки. Дисертація не говорить вам точно про те, як зробити конверсію, але вона говорить, що це, безумовно, можливо.

У багатьох випадках перетворення рекурсивної функції легко. Кнут пропонує кілька прийомів у «Мистецтві комп’ютерного програмування». І часто річ, обчислена рекурсивно, може бути обчислена зовсім іншим підходом за менший час та простір. Класичним прикладом цього є числа Фібоначчі або їх послідовності. Ви напевно зіткнулися з цією проблемою у своєму плані ступеня.

Зворотній бік цієї монети, ми, звичайно, можемо уявити, що система програмування настільки вдосконалена, щоб трактувати рекурсивне визначення формули як запрошення на запам'ятовування попередніх результатів, пропонуючи таким чином перевагу швидкості без клопоту сказати комп’ютеру, які саме кроки потрібно робити слідуйте при обчисленні формули з рекурсивним визначенням. Дайкстра майже напевно уявляв собі таку систему. Він тривалий час намагався відокремити реалізацію від семантики мови програмування. Потім знову його недетерміновані та багатопроцесорні мови програмування перебувають у лізі вище професійного програміста.

Зрештою, багато функцій просто простіші для розуміння, читання та запису в рекурсивній формі. Якщо немає вагомих причин, ви, ймовірно, не повинні (вручну) перетворювати ці функції в явно ітеративний алгоритм. Ваш комп’ютер буде правильно виконувати цю роботу.

Я бачу одну вагому причину. Припустимо, у вас є прототип системи на мові надвисокого рівня, як [ надягаючи азбестову білизну ] Схеми, Лісп, Хаскелл, OCaml, Perl або Паскаль. Припустимо, умови такі, що вам потрібна реалізація на C або Java. (Можливо, це політика.) Тоді ви, безумовно, можете мати деякі функції, написані рекурсивно, але які, в перекладі буквально, підірвали б вашу систему виконання. Наприклад, у схемі можлива нескінченна рекурсія хвоста, але та сама ідіома спричиняє проблеми для існуючих середовищ C. Іншим прикладом є використання лексично вкладених функцій та статичного діапазону, який Паскаль підтримує, але C - ні.

У цих умовах ви можете спробувати подолати політичний опір оригінальній мові. Можливо, вам здасться погано реалізовувати Лісп, як у десятому законі Грінспуна (язик у щоку). Або ви просто можете знайти зовсім інший підхід до рішення. Але в будь-якому випадку, безумовно, є спосіб.


10
Хіба Церква-Тьюрінг ще не доведена?
Ліран Ореві

15
@eyelidlessness: Якщо ви можете реалізувати A в B, це означає, що B має принаймні стільки ж потужності, як А. Якщо ви не можете виконати якийсь оператор A в A-реалізації-of-B, то це не реалізація. Якщо A може бути реалізовано в B, а B може бути реалізовано в A, потужність (A)> = потужність (B), а потужність (B)> = потужність (A). Єдине рішення - потужність (A) == потужність (B).
Тордек

6
re: 1-й абзац: Ви говорите про еквівалентність моделей обчислення, а не тезу Церкви Тьюрінга. Еквівалентність AFAIR була доведена Церквою та / або Тюрінгом, але це не теза. Теза - це експериментальний факт, що все, що інтуїтивно обчислюється, обчислюється в суворому математичному сенсі (машинами Тьюрінга / рекурсивними функціями тощо). Це може бути спростовано, якби за допомогою законів фізики ми могли б створити деякі некласичні комп’ютери, що обчислюють щось, що машини Тьюрінга не можуть зробити (наприклад, проблема зупинки). Тоді як еквівалентність є математичною теоремою, і це не буде спростовано.
sdcvvc

7
Як чорт за цю відповідь отримав позитивні голоси? Спочатку він змішує повноту Тюрінга з тезою Церкви-Тьюрінга, потім робить купу неправильних рукодільниць, згадуючи про «просунуті» системи та відкидаючи ледачу нескінченну рекурсію хвоста (що ви можете зробити на С або будь-якій мові Тюрінга на повній мові, тому що ... е. Хтось знає, що означає Тюрінг?). Тоді надійний висновок, тримаючи руку, як це було питання про Опра, і все, що вам потрібно, - бути позитивним і піднесеним? Жартівлива відповідь!
ex0du5

8
А бс про семантику ??? Дійсно? Це питання про синтаксичні перетворення, і якимось чином це став чудовим способом назвати крапку Dijkstra і означати, що ви знаєте щось про пі-числення? Дозвольте пояснити: чи дивитися на денотаційну семантику мови чи якусь іншу модель не матиме ніякого відношення до відповіді на це питання. Незалежно від того, мова є збірною чи мовою генеративного домену, нічого не означає. Йдеться лише про повноту Тьюрінга та перетворення "змінних стеків" в "стек змінних".
ex0du5

43

Чи завжди можна написати нерекурсивну форму для кожної рекурсивної функції?

Так. Простим офіційним доказом є показати, що як µ рекурсія, так і нерекурсивне числення, як-от GOTO, є Тюрінга завершеними. Оскільки всі повні розрахунки Тьюрінга суворо еквівалентні за своєю експресивною силою, усі рекурсивні функції можуть бути реалізовані нерекурсивним численням Тюрінга.

На жаль, я не можу знайти хороше, формальне визначення GOTO в Інтернеті, ось ось таке:

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

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

Однак перетворення між рекурсивними та нерекурсивними функціями не завжди є тривіальними (за винятком бездумної ручної повторної реалізації стека викликів).

Для отримання додаткової інформації див цю відповідь .


Чудова відповідь! Однак на практиці у мене виникають великі труднощі з перетворенням рекурсивних альго в ітеративні. Наприклад, я не зміг поки що перетворити мономорфний типер, представлений тут community.topcoder.com/…, в ітеративний алгоритм
Нілс,

31

Рекурсія реалізується у вигляді стеків або подібних конструкцій у власних інтерпретаторах чи укладачах. Таким чином, ви, звичайно, можете перетворити рекурсивну функцію в ітеративний аналог, тому що це завжди робиться (якщо автоматично) . Ви просто будете дублювати роботу компілятора тимчасово і, ймовірно, дуже потворно і неефективно.


13

В основному, так, по суті, ви, в кінцевому підсумку, повинні це замінити виклики методу (які неявно підштовхують стан до стека) на явні стеки підштовхує, щоб пам’ятати, де потрапив «попередній виклик», а потім виконати «названий метод» замість цього.

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


9
  • Потік виконання рекурсивної функції може бути представлений у вигляді дерева.

  • Цю ж логіку може виконувати цикл, який використовує структуру даних для переходу до цього дерева.

  • Перехід по глибині перший може бути виконаний за допомогою стека, обхід першого ширини може бути виконаний за допомогою черги.

Отже, відповідь: так. Чому: https://stackoverflow.com/a/531721/2128327 .

Чи може бути проведена будь-яка рекурсія в одному циклі? Так, тому що

машина Тьюрінга робить все, що робить, виконуючи один цикл:

  1. отримати інструкцію,
  2. оцініть це,
  3. goto 1.

7

Так, явно використовуючи стек (але рекурсію набагато приємніше читати, IMHO).


17
Я б не сказав, що завжди приємніше читати. Ітерація, і рекурсія мають своє місце.
Меттью Флашен

6

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


Що або перемагає мету, якщо структура даних ваших стеків розподілена на стеку, або проходить довше, якщо вона розподілена на купі, ні? Це звучить для мене банально, але неефективно.
conradkleinespel

1
@conradk У деяких випадках це практична річ, якщо потрібно виконати деяку дерево-рекурсивну операцію над проблемою, достатньо великою, щоб вичерпати стек викликів; пам'ять купи зазвичай набагато рясніша.
jamesdlin

4

У принципі, завжди можна видалити рекурсію та замінити її ітерацією мовою, що має нескінченний стан як для структури даних, так і для стека викликів. Це є основним наслідком тези Церкви-Тьюрінга.

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


2

Всі обчислювані функції можуть бути обчислені машинами Тьюрінга, отже, рекурсивні системи та машини Тьюрінга (ітераційні системи) є рівнозначними.


1

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

Зараз це менше проблеми, оскільки мода змінилася на інші технології.



0

Крім чіткого стека, ще одна модель для перетворення рекурсії в ітерацію - це використання батута.

Тут функції або повертають кінцевий результат, або закривають виклик функції, який він інакше виконував би. Потім функція ініціювання (трамплінування) продовжує викликати повернені закриття до досягнення кінцевого результату.

Цей підхід працює для взаємно рекурсивних функцій, але я боюся, що він працює лише для викликів хвостів.

http://en.wikipedia.org/wiki/Trampoline_(computers)


0

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


1
Я думаю, що ОП шукає доказ чи щось інше суттєве
Тім

0

Погляньте на наступні записи у Вікіпедії, ви можете використовувати їх як вихідну точку, щоб знайти повну відповідь на своє запитання.

Дотримується абзацу, який може дати вам підказку, з чого почати:

Розв’язання відношення рецидиву означає отримання рішення закритої форми : нерекурсивна функція n.

Подивіться також останній абзац цього запису .


0

Можна перетворити будь-який рекурсивний алгоритм в нерекурсивний, але часто логіка набагато складніша, і для цього потрібно використовувати стек. Насправді сама рекурсія використовує стек: стек функцій.

Детальніше: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions


-1

tazzego, рекурсія означає, що функція буде називати себе, подобається вам це чи ні. Коли люди говорять про те, чи можна робити речі без рекурсії, вони мають на увазі це, і ви не можете сказати «ні, це неправда, тому що я не згоден з визначенням рекурсії» як дійсне твердження.

Зважаючи на це, майже все, що ви говорите, - це нісенітниця. Єдине, що ви говорите, що це нісенітниця, це думка, що ви не можете уявити програмування без стоп-сигналу. Це те, що робилося десятиліттями до тих пір, поки використання стакану виклику не стало популярним. У старих версіях FORTRAN не вистачало дзвінка, і вони працювали чудово.

До речі, існують мови-повні Тьюрінга, які реалізують лише рекурсію (наприклад, SML) як засіб циклічного циклу. Існують також мови, що повністю завершують Тьюрінг, які реалізують лише ітерацію як засіб циклічного циклу (наприклад, FORTRAN IV). Теза Церкви Тьюрінга доводить, що все можливе на мовах, що мають лише рекурсію, може бути зроблено нерекурсивною мовою, а навпаки, тим, що вони мають властивість цілісного завершення.


-3

Ось ітеративний алгоритм:

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.