Мені було цікаво, чи цикл певного часу є суто рекурсією?
Я думаю, це тому, що певний час цикл може розглядатися як функція, яка викликає себе в кінці. Якщо це не рекурсія, то в чому різниця?
Мені було цікаво, чи цикл певного часу є суто рекурсією?
Я думаю, це тому, що певний час цикл може розглядатися як функція, яка викликає себе в кінці. Якщо це не рекурсія, то в чому різниця?
Відповіді:
Петлі - це не рекурсія. Насправді вони є яскравим прикладом протилежного механізму: ітерації .
Суть рекурсії полягає в тому, що один елемент обробки викликає інший екземпляр себе. Техніка управління петлею просто відскакує назад до тієї точки, з якої вона почалася.
Стрибки в коді та виклик іншого блоку коду - це різні операції. Наприклад, коли ви переходите до початку циклу, змінна керування циклом все ще має те саме значення, що і до стрибка. Але якщо ви викликаєте інший екземпляр розповсюдження, в якому ви перебуваєте, то новий екземпляр має нові, не пов'язані між собою копії всіх його змінних. Ефективно, одна змінна може мати одне значення на першому рівні обробки та інше значення на нижчому рівні.
Ця здатність має вирішальне значення для роботи багатьох рекурсивних алгоритмів, і саме тому ви не можете емулювати рекурсію за допомогою ітерації, не керуючи також стеком названих кадрів, який відслідковує всі ці значення.
Якщо сказати, що X є власне Y, має сенс лише якщо у вас є якась (формальна) система, що ви виражаєте X. Якщо ви визначаєте семантику while
точки зору обчислення лямбда, ви можете згадати рекурсію *; якщо ви визначите це з допомогою машини реєстрації, ви, ймовірно, не зробите.
У будь-якому випадку люди, ймовірно, не зрозуміють вас, якщо ви називатимете функцію рекурсивної лише тому, що вона містить цикл "час".
* Хоча, можливо, лише побічно, наприклад, якщо ви визначаєте це в термінах fold
.
while
конструкцією рекурсивність, як правило, є властивістю функцій, я просто не можу придумати нічого іншого, щоб описати як "рекурсивний" у цьому контексті.
Це залежить від вашої точки зору.
Якщо поглянути на теорію обчислюваності , то ітерація та рекурсія однаково виражають . Це означає, що ви можете написати функцію, яка щось обчислює, і неважливо, чи будете ви це робити рекурсивно чи ітеративно, ви зможете вибрати обидва підходи. Не можна нічого обчислювати рекурсивно, що не можна ітераційно обчислювати, і навпаки (хоча внутрішня робота програми може бути різною).
Багато мов програмування не відносяться до рекурсії та ітерації однаково і з поважних причин. Зазвичай рекурсія означає, що мова / компілятор обробляє стек викликів, а ітерація означає, що вам, можливо, доведеться самостійно обробляти стеки.
Однак є мови, особливо функціональні мови, в яких такі речі, як петлі (для, поки), є насправді лише синтаксичним цукром для рекурсії та реалізуються за лаштунками таким чином. Це часто бажано у функціональних мовах, оскільки вони, як правило, не мають принципу циклічного циклу, і додавання цього зробить їх обчислення складнішим, з невеликих практичних причин.
Так ні, вони не є сутнісно однаковими . Вони однаково виразні , тобто ви не можете обчислити щось ітеративно, ви не можете обчислити рекурсивно і навпаки, але це стосується цього в загальному випадку (згідно із тезою Церкви Тьюрінга).
Зауважте, що ми говоримо про рекурсивні програми тут. Існують і інші форми рекурсії, наприклад, в структурах даних (наприклад, дерева).
Якщо дивитися на це з точки зору реалізації , то рекурсія та ітерація майже не однакові. Рекурсія створює новий кадр стека для кожного дзвінка. Кожен крок рекурсії є самостійним, отримуючи аргументи для обчислення від виклику (самого).
Петлі з іншого боку не створюють кадри виклику. Для них контекст не зберігається на кожному кроці. Для циклу програма просто стрибає до початку циклу, поки не завершиться стан циклу.
Це досить важливо знати, оскільки це може зробити досить радикальні відмінності в реальному світі. Для рекурсії на кожному дзвінку потрібно зберігати весь контекст. Для ітерації ви маєте точний контроль того, які змінні знаходяться в пам'яті, а що зберігається.
Якщо ви дивитесь на це таким чином, ви швидко бачите, що для більшості мов ітерація та рекурсія принципово відрізняються і мають різні властивості. Залежно від ситуації деякі властивості є більш бажаними, ніж інші.
Рекурсія може зробити програми більш простими та легшими для перевірки та підтвердження . Перетворення рекурсії в ітерацію зазвичай робить код більш складним, збільшуючи ймовірність відмови. З іншого боку, перетворення на ітерацію та зменшення кількості кадрів стека викликів може заощадити дуже потрібну пам'ять.
Різниця полягає в неявному стеку та смисловості.
У циклі "час", який "дзвонить собі в кінці", немає стека, щоб він повзав назад, коли це зроблено. Остання остання ітерація встановлює, яким буде стан, як закінчується.
Однак рекурсію неможливо виконати без цього неявного стеку, який пам’ятає про стан роботи, виконаний раніше.
Це правда, що ви можете вирішити будь-яку проблему рекурсії за допомогою ітерації, якщо явно надаєте їй доступ до стеку. Але робити це таким чином - не те саме.
Семантична різниця пов'язана з тим, що перегляд рекурсивного коду передає ідею зовсім інакше, ніж ітераційний код. Ітеративний код робить речі крок за часом. Він приймає будь-який стан, який виходив з раніше, і працює лише для створення наступного стану.
Рекурсивний код розбиває проблему на фрактали. Ця маленька частина схожа на цю велику частину, тому ми можемо робити саме цей шматочок і той самий біт так само. Це інший спосіб думати про проблеми. Це дуже потужно і вимагає звикання. Багато можна сказати в декількох рядках. Ви просто не можете вийти з цього циклу, навіть якщо він має доступ до стеку.
Все залежить від використання терміна по суті . На рівні мови програмування вони синтаксично та семантично відрізняються, і вони мають зовсім іншу продуктивність та використання пам’яті. Але якщо ви копаєтесь досить глибоко в теорії, їх можна визначити один з одним, і, отже, в одному теоретичному сенсі "однаковий".
Справжнє питання: Коли має сенс розрізняти ітерацію (петлі) і рекурсію, і коли корисно думати про неї як про одні й ті ж речі? Відповідь полягає в тому, що при фактичному програмуванні (на відміну від написання математичних доказів) важливо розрізняти ітерацію та рекурсію.
Рекурсія створює новий кадр стека, тобто новий набір локальних змінних для кожного виклику. Це має накладні витрати і займає місце на стеку, а це означає, що досить глибока рекурсія може переповнити стек, що спричинить збій програми. Ітерація, з іншого боку, лише змінює існуючі змінні, тому, як правило, швидше і займає лише постійну кількість пам'яті. Тож це дуже важлива відмінність для розробника!
У мовах з рекурсією хвостового виклику (як правило, функціональні мови) компілятор, можливо, зможе оптимізувати рекурсивні виклики таким чином, що вони забирають лише постійну кількість пам'яті. У цих мовах важливою відмінністю є не ітерація проти рекурсії, а версія без хвоста-виклику-рекурсія хвост-рекурс та ітерація.
Підсумок: Вам потрібно вміти визначити різницю, інакше ваша програма вийде з ладу.
while
петлі є формою рекурсії, див., наприклад, прийняту відповідь на це питання . Вони відповідають μ-оператору в теорії обчислень (див., Наприклад, тут ).
Усі варіанти for
циклів, які повторюються на діапазоні чисел, кінцевій колекції, масиві тощо, відповідають примітивній рекурсії, див., Наприклад, тут і тут . Зауважте, що for
петлі C, C ++, Java та ін., Насправді є синтаксичним цукром для while
циклу, і тому це не відповідає примітивній рекурсії. for
Петля Паскаля - приклад примітивної рекурсії.
Важлива відмінність полягає в тому, що примітивна рекурсія завжди припиняється, тоді як генералізована рекурсія ( while
петлі) може не закінчитися.
EDIT
Деякі роз’яснення щодо коментарів та інші відповіді. "Рекурсія виникає, коли річ визначається з точки зору самої себе або її типу". (див. Вікіпедію ). Так,
Чи цикл часу деякий час є рекурсією?
Оскільки ви можете визначити while
цикл з точки зору себе
while p do c := if p then (c; while p do c))
то, так , while
петля - це форма рекурсії. Рекурсивні функції - це ще одна форма рекурсії (інший приклад рекурсивного визначення). Списки та дерева - це інші форми рекурсії.
Ще одне питання, на яке явно передбачається багато відповідей та коментарів, є
Чи одночасно циклічні та рекурсивні функції рівноцінні?
Відповідь на це питання - ні : while
цикл відповідає хвостово-рекурсивній функції, де змінні, до яких звертається цикл, відповідають аргументам неявної рекурсивної функції, але, як уже вказували інші, не хвостово-рекурсивні функції не може бути змодельована while
циклом без використання додаткового стека.
Отже, той факт, що " while
цикл - це форма рекурсії", не суперечить тому, що "деякі рекурсивні функції не можуть бути виражені while
циклом".
FOR
циклом може обчислити саме всі примітивні рекурсивні функції, а мова з простою WHILE
циклом може обчислити саме всі µ-рекурсивні функції (і виявляється, що µ-рекурсивні функції - це саме ті функції, які Машина Тьюрінга може обчислити). Або, якщо коротко: примітивна рекурсія та µ-рекурсія - це технічні терміни з математики / теорії обчислення.
Хвіст виклик (або хвіст рекурсивний виклик) точно реалізований як «Гото з аргументами» (без натискання будь - якої додатковий кадру виклику на стеку викликів ) , а в деяких функціональних мовах (Ocaml в зокрема) є звичайним способом зациклення.
Тож певний час (у мовах, що їх мають) може розглядатися як закінчення хвостовим викликом до його тіла (або його тестом на голову).
Точно так само звичайні рекурсивні дзвінки (без зворотного дзвінка) можуть бути імітовані циклами (використовуючи деякий стек).
Читайте також про продовження та стиль продовження проходження .
Тож "рекурсія" та "ітерація" глибоко рівнозначні.
Це правда, що як рекурсія, так і без обмежених циклів у той час є рівнозначними з точки зору обчислювальної виразності. Тобто, будь-яка програма, написана рекурсивно, може бути переписана в еквівалентну програму, використовуючи натомість петлі, і навпаки. Обидва підходи є повним завершенням , тобто вони можуть бути використані для обчислення будь-якої обчислювальної функції.
Принципова різниця в плані програмування полягає в тому, що рекурсія дозволяє використовувати дані, що зберігаються в стеку викликів. Щоб проілюструвати це, припустимо, що ви хочете надрукувати елементи спільно пов'язаного списку, використовуючи цикл або рекурсію. Я буду використовувати C для прикладу коду:
typedef struct List List;
struct List
{
List* next;
int element;
};
void print_list_loop(List* l)
{
List* it = l;
while(it != NULL)
{
printf("Element: %d\n", it->element);
it = it->next;
}
}
void print_list_rec(List* l)
{
if(l == NULL) return;
printf("Element: %d\n", l->element);
print_list_rec(l->next);
}
Просте, правда? Тепер зробимо одну невелику модифікацію: надрукуйте список у зворотному порядку.
Для рекурсивного варіанту це майже тривіальна модифікація вихідної функції:
void print_list_reverse_rec(List* l)
{
if (l == NULL) return;
print_list_reverse_rec(l->next);
printf("Element: %d\n", l->element);
}
Однак для функції циклу у нас є проблема. Наш список пов'язаний між собою, тому його можна просунути лише вперед. Але оскільки ми друкуємо зворотним шляхом, ми повинні почати друкувати останній елемент. Як тільки ми дійшли до останнього елемента, ми вже не можемо повернутися до другого до останнього елемента.
Таким чином, ми або мусимо зробити багато ретрансляції, або ми повинні побудувати допоміжну структуру даних, яка б відстежувала відвідувані елементи і з якої ми зможемо потім ефективно друкувати.
Чому ми не маємо цієї проблеми з рекурсією? Оскільки в рекурсії у нас вже є допоміжна структура даних: Стек виклику функції.
Оскільки рекурсія дозволяє нам повернутися до попереднього виклику рекурсивного виклику, при цьому всі локальні змінні та стан для цього виклику залишаються неушкодженими, ми отримуємо певну гнучкість, яку було б нудно моделювати в ітеративному випадку.
Петлі - це особлива форма рекурсії для досягнення конкретного завдання (переважно ітерації). Можна реалізувати цикл у рекурсивному стилі з однаковою продуктивністю [1] кількома мовами. а в SICP [2] ви бачите, як петлі описані як "синтастичний цукор". У більшості імперативних мов програмування блоки для і в той час використовують ті ж області, що і їх батьківська функція. Тим не менш, у більшості функціональних мов програмування не існує ні циклів, ні циклів, оскільки в них немає потреби.
Причина, якою імперативні мови є для циклів / while, - це те, що вони обробляють стани, змінюючи їх. Але насправді, якщо ви дивитесь з іншої точки зору, якщо ви думаєте про блокування час як саму функцію, беручи параметр, обробляйте його та повертаючи новий стан - що може бути викликом тієї самої функції з різними параметрами - ви можна думати про петлю як про рекурсію.
Світ можна також визначити як змінний або незмінний. якщо ми визначимо світ як набір правил, і назвемо кінцеву функцію, яка приймає всі правила, і поточний стан як параметри, і повернемо новий стан відповідно до цих параметрів, який має однаковий функціонал (генерувати наступний стан у тому ж самому спосіб), ми могли б також сказати, що це рекурсія і цикл.
у наступному прикладі "life" функція приймає два параметри "правила" та "стан", і новий стан буде побудовано в наступному галочку.
life rules state = life rules new_state
where new_state = construct_state_in_time rules state
[1]: оптимізація хвостових викликів - це звичайна оптимізація функціональних мов програмування для використання існуючого стека функцій у рекурсивних викликах замість створення нового.
[2]: Структура та інтерпретація комп'ютерних програм, MIT. https://mitpress.mit.edu/books/structure-and-interpretation-computer-programs
Певний час цикл відрізняється від рекурсії.
Коли викликається функція, відбувається наступне:
До стека додається рамка стека.
Покажчик коду переміщується на початок функції.
Коли цикл деякий час знаходиться в кінці, відбувається наступне:
Умова запитує, чи є щось правдою.
Якщо так, код переходить до точки.
Взагалі цикл while схожий на наступний псевдокод:
if (x)
{
Jump_to(y);
}
Найголовніше, що у рекурсії та циклів є різні представлення коду складання та машинного коду. Це означає, що вони не однакові. Вони можуть мати однакові результати, але різний машинний код доводить, що вони не на 100% одне і те ж.
Просто ітерація недостатня, щоб вона загалом була еквівалентною рекурсії, але ітерація зі стеком взагалі еквівалентна. Будь-яка рекурсивна функція може бути перепрограмована як ітеративна петля зі стеком, і навпаки. Це не означає, що це практично, але в будь-якій конкретній ситуації одна чи інша форма може мати явні переваги над іншою версією.
Я не впевнений, чому це суперечливо. Рекурсія та ітерація зі стеком - це той самий обчислювальний процес. Вони те саме «явище», так би мовити.
Єдине, що я можу придумати, це те, що, розглядаючи їх як "інструменти програмування", я погоджуюся, що ви не повинні думати про них як про одне і те ж. Вони є "математично" або "обчислювально" еквівалентними (знову-таки ітерація стеком , а не ітерація в цілому), але це не означає, що вам слід підходити до проблем з думкою, що буде робити хтось із них. З точки зору впровадження / вирішення проблем, деякі проблеми можуть працювати краще так чи інакше, і ваша робота як програміста - правильно визначити, яка з них краще підходить.
Для уточнення, відповідь на запитання Чи цикл часу деякий час є рекурсією? це певне ні , або, принаймні, "ні, якщо ви також не маєте стека".