Видалення рекурсії - погляд в теорію за лаштунками


10

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

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

Будучи трохи більш конкретним, я хотів би (формально) побачити, як можна довести це твердження у випадках, коли у вас функція викликає ланцюг . Далі, що робити, якщо є якісь умовні висловлювання, які можуть призвести до того, що може зателефонувати на якийсь ? Тобто графік виклику потенційної функції має деякі сильно пов'язані компоненти.F i F jF0F1FiFi+1FnF0FiFj

Мені хотілося б знати, як можна вирішити ці ситуації, скажімо, якийсь рекурсивний до ітеративного перетворювача. А хіба рукохвильовий опис, про який я згадував раніше, справді достатньо для цієї проблеми? Я маю на увазі, чому тоді я вважаю, що видалити рекурсію в деяких випадках легко. Зокрема, видалити рекурсію з обходу бінарного дерева попереднього замовлення дуже просто - його стандартне запитання про інтерв'ю, але видалення рекурсії у випадку після замовлення завжди було для мене кошмаром.

Що я насправді задаю - це питання2

(1) Чи дійсно є більш формальне (переконливе?) Доказ того, що рекурсія може бути перетворена на ітерацію?

(2) Якщо ця теорія справді існує там, то чому я вважаю, що, наприклад, ітератизація попереднього замовлення легше, а після замовлення так важко? (крім мого обмеженого інтелекту)


1
як слово ітератизуюче :)
Акаш Кумар

Я не впевнений, чи я повністю розумію, але якщо рекурсія десь закінчиться, то ви можете насправді імітувати системний стек, використовуючи власний стек. За частиною (2) проблеми не відрізняються з точки зору складності обчислень.
singhsumit

Я думаю, що це питання найкраще підходило б для сайту Computer Science, який ще не існує. Що стосується вашого другого питання, чи можете ви уточнити, чому ви вважаєте, що це важче? Процес повинен бути майже однаковим.
Рафаель

дякую всім за ваші коментарі - гадаю, у мене є чимало читання.
Ітачі Учіха

@Raphael - Один коментар про те, чому я вважаю, що повторне замовлення є важким (до того ж я не в змозі це зробити). Я читав кілька статей про видалення рекурсії і натрапив на щось, що називається хвостовими рекурсивними функціями. Виявляється, їх простіше ітератизувати. Я досі формально не розумію, чому це правда; але є ще одна річ, яку я повинен додати. Я чув, що ітератизація після замовлення вимагає двох стеків і не одну, але не знаю деталей. І тепер я загублений - чому така різниця між цими двома режимами обходу? І чому з хвостовою рекурсією легко впоратися?
Ітачі Учіха

Відповіді:


6

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

Так що у нас є «виклик ланцюга» . Якщо ми припустимо також, що самі по собі не є рекурсивними (тому що ми їх вже перетворили), ми можемо вкласти всі ці виклики у визначення яке, таким чином, стає безпосередньо рекурсивною функцією, з якою ми вже можемо мати справу.F 1 , , F n FFF1FnFF1,,FnF

Це не вдається, якщо деякий має собі рекурсивний ланцюг викликів, у якому виникає , тобто . У цьому випадку у нас взаємна рекурсія, що вимагає чергового фокусу, щоб позбутися. Ідея полягає в тому, щоб обчислити обидві функції одночасно. Наприклад, у тривіальному випадку: F F jF F jFjFFjFFj

f(0) = a
f(n) = f'(g(n-1))

g(0) = b
g(n) = g'(f(n-1))

з f'і g'нерекурсивними функціями (або принаймні незалежними від fі g) стає

h(0) = (a,b)
h(n) = let (f,g) = h(n-1) in (f'(g), g'(f)) end

f(n) = let (f, _) = h(n) in f end
g(n) = let (_, g) = h(n) in g end

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


Радий, що можу допомогти. Будь ласка, не забудьте прийняти свою улюблену відповідь, натиснувши на неї прапорець.
Рафаель

1
Рафеле, ваш трюк працює лише тоді, коли обидві рекурсивні функції приймають аргументи одного типу. Якщо fі gприймати різні види типів, більш загальний трюк необхідний.
Андрій Бауер

@AndrejBauer гарне спостереження, я це зовсім пропустив. мені дуже сподобався підхід Рафаеля, але, як ви спостерігали в загальних випадках, нам, мабуть, потрібна якась інша ідея. Чи можете ви зробити якісь інші пропозиції?
Ітачі Учіха

fgn1n2

Ну, дивіться мою відповідь, як це зробити.
Андрій Бауер

8

Так, є переконливі причини вважати, що рекурсію можна перетворити на ітерацію. Це робить кожен компілятор, коли він переводить вихідний код на машинну мову. Для теорії слід дотримуватися пропозицій Дейва Кларка. Якщо ви хочете побачити фактичний код, який перетворює рекурсію в нерекурсивний код, machine.mlознайомтесь з мовою MiniML в моєму зоопарку PL (зауважте, що loopфункція внизу, яка фактично запускає код, є хвостовою рекурсивною, і тому вона може бути тривіально перетвореним у фактичний цикл).

І ще одна річ. MiniML не підтримує взаємно рекурсивні функції. Але це не проблема. Якщо у вас взаємна рекурсія між функціями

f1:A1B1
f2:A2B2
fn:AnBn

рекурсія може бути виражена у вигляді єдиної рекурсивної карти

f:A1++AnB1++Bn,

8

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

Пов'язаний підхід - це машина CEK .

Вони обидва давно існують, тому над ними багато роботи. І звичайно, є докази того, що вони працюють, і процедура "компілювання" програми в інструкції SECD є лінійною за розміром програми (вона не повинна думати про програму).

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


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

[cntd] Я маю на увазі, що мені сподобається доказ, якщо такий є, щоб сказати мені, чому ітератизувати одну програму простіше, ніж іншу. Але в деякому сенсі рекурсивно-ітеративний перетворювач повинен працювати незалежно від того, яку рекурсивну програму він приймає як вхід. Не впевнений, але я гадаю, що зробити такий перетворювач може бути важким, як проблема зупинки? Я просто здогадуюсь тут - але мені б хотілося, щоб існував рекурсивний ітеративний перетворювач, і якщо це буде, я хотів би, щоб він пояснив властиву йому складність ітератизації різних рекурсивних програм. я не впевнений, але чи слід редагувати питання? Чи ясно моє запитання?
Ітачі Учіха

@ItachiUchiha - Я не думаю, що ваша проблема не вирішена. Подивіться на відповідь Андрія Бауера. Він зазначає, що кожен компілятор робить це, коли переводить вихідний код на машинну мову. Також він додає, що ви можете побачити фактичний код, який перетворює рекурсивну в нерекурсивну на мові MiniM (a) l. Це чітко вказує на те, що існує процедура прийняття рішення щодо "ітералізації" рекурсії. Я не впевнений у властивій (концептуальній) складності / складності зняття рекурсії. Я не розумію це питання дуже чітко, але це виглядає цікаво. Можливо, ви можете відредагувати своє запитання, щоб отримати кращу відповідь
Акаш Кумар

Суть моєї відповіді полягає в тому, що існує автоматична процедура робити те, що ви хочете. На жаль, трансформація не обов'язково буде стосуватися речей, які програмісту негайно інтерпретувати. Я думаю, що головне в тому, що, коли ви хочете ітеризувати програму, вам потрібно зберегти на стеці, що програма повинна робити, коли ви повертаєтесь з ітеризованого виклику функції (це називається продовженням). Для деяких функцій (таких як хвостово-рекурсивні функції) продовження тривіальне. Для інших продовження може бути дуже складним, особливо якщо вам доведеться кодувати його самостійно.
Дейв Кларк

6

Питання : "Чи дійсно є більш офіційний (переконливий?) Доказ того, що рекурсія може бути перетворена на ітерацію?"

A : Повнота Тюрінга в повноті машини :-)

Жартуючи окремо, модель машини Turing за еквівалентним випадковим доступом (RASP) близька до того, як працюють справжні мікропроцесори, а його набір інструкцій містить лише умовний стрибок (без рекурсії). Можливість динамічно самовиправляти код полегшує завдання реалізації підпрограм та рекурсивних викликів.

Я думаю, що ви можете знайти багато робіт / статей про " рекурсивно-ітераційне перетворення " (див. Відповідь Дейва або просто ключові слова Google), але, мабуть, менш відомийпрактичний ) підхід - це останнє дослідження апаратної реалізації рекурсивних алгоритмів ( використовуючи мову VHDL, яка "компілюється" безпосередньо в апаратну частину). Наприклад, див. Статтю В. Склярова " Реалізація рекурсивних алгоритмів на основі FPGA " ( У статті запропоновано новий метод реалізації рекурсивних алгоритмів в апараті. .... Досліджено два практичні програми рекурсивних алгоритмів у області сортування даних та стиснення даних. докладно .... ).


1

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

Перетворення CPS тісно пов'язане з явним збереженням стека викликів у традиційному стеці на основі масиву, але замість масиву стек виклику представлений зв'язаними закриттями.


0

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

Як зазначається у статті Петра Ландіна з 1965 р. Листування між ALGOL 60 та церквою Ламбда-нотації, послідовну мову процедурного програмування можна зрозуміти з точки зору обчислення лямбда, що забезпечує основні механізми процедурного абстрагування та застосування процедури (підпрограми).

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

Розсер, Дж. Б. (1939). "Неофіційне виклад доказів теореми Годеля та теореми Церкви". Журнал символічної логіки (Журнал символічної логіки, т. 4, № 2) 4 (2): 53–60. doi: 10.2307 / 2269059. JSTOR 2269059.

Зауважте, звичайно, для всіх, хто цікавиться принципами сучасної мови Lisp та варіантової схеми, навмисно мають сильну схожість з обчисленням лямблі. Вивчення коду інтерпретатора для оцінки вираження призводить до ідей, які спочатку містилися в документах про повноту вичислення лямбда.


1
Доказ еквівалентності Тьюрінга / лямбда є у додатку до цієї статті: www.cs.virginia.edu/~robins/Turing_Paper_1936.pdf
Radu GRIGо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.