Рекурсія або петлі під час


123

Я читав про деякі практики інтерв'ю з розвитку, зокрема про технічні питання та тести, задані на інтерв'ю, і я досить багато разів натрапляв на приказки жанру "Добре, ви вирішили проблему з певного циклу, тепер ви можете це зробити з рекурсія ", або" кожен може вирішити це за допомогою 100 рядків під час циклу, але чи можуть вони це робити в 5-ти рядкових рекурсивних функціях? " тощо.

Моє запитання: чи рекурсія в цілому краща, ніж якщо / поки / для конструкцій?

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



73
Що стосується рекурсії, це виглядає досить цікаво.
dan_waterworth

4
@dan_waterworth також, це допомогло б: google.fr/…, але я, здається, завжди промальовую це: P
Shivan Dragon

@ShivanDragon Я вважав так ^ _ ^ Дуже доречно, що я опублікував це вчора :-)
Ніл

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

Відповіді:


192

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

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

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

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

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

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


3
Мені найбільше подобається ваша відповідь, оскільки вона торкається найбільш важливих аспектів, які могли б відповісти на це питання, пояснює основні технічні частини, а також дає хороший зменшений погляд на те, як ця проблема вписується у сферу програмування.
Дракон Шиван

1
Я також помітив схему в програмі синхронізації, де циклів уникає на користь рекурсивних викликів ітератора, що містить метод .next (). Я припускаю, що довгий час роботи код не стає занадто жадібним процесором.
Еван Плейс

1
Ітераційна версія також передбачає виштовхування та з'ясування значень. Вам просто потрібно вручну написати код, щоб це зробити. Рекурсивна версія - це натискання стану на стек в ітеративному алгоритмі, який, як правило, доводиться вручну моделювати, пересуваючи стан у якусь структуру. Лише найтривіальніші алгоритми не потребують цього стану, і в цих випадках компілятор зазвичай може помітити хвостову рекурсію і нанести ітераційне рішення.
Мартін Йорк

1
@tdammers Чи не могли б ви сказати мені, де я можу прочитати згадане вами дослідження «Досвід та дослідження свідчать про те, що між людьми існує лінія ...» Це здається мені дуже цікавим.
Yoo Matsuo

2
Одне, що ви забули згадати, ітераційний код має кращі результати, коли ви маєте справу з одним потоком виконання, але рекурсивні алгоритми, як правило, піддаються виконанню в декількох потоках.
GordonM

37

Це залежить.

  • Деякі проблеми дуже піддаються рекурсивним рішенням, наприклад, швидкий вибір
  • Деякі мови насправді не підтримують рекурсію, наприклад, ранні FORTRAN
  • Деякі мови передбачають рекурсію як основний засіб для циклічного циклу, наприклад, Haskell

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

Також рекурсивний алгоритм завжди можна реалізовувати ітеративно , використовуючи явний стек .

Нарешті, я зазначу, що п'ятирядковий варіант, ймовірно, завжди кращий, ніж 100-рядковий (якщо припустити, що вони є рівнозначними).


5
Приємна відповідь (+1). "5-ти рядкове рішення, ймовірно, завжди краще, ніж 100-лінійне": Я думаю, стислість - не єдина перевага рекурсії. Використання рекурсивного виклику змушує чітко визначити функціональні залежності між значеннями в різних ітераціях.
Джорджіо

4
Коротші рішення, як правило, є кращими, але є така річ, як надмірно стислі.
dan_waterworth

5
@dan_waterworth порівняно зі "100 рядком" досить важко бути надмірно стислим
gnat

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

1
@jk, я думаю, це ще одна форма зробити явну інформацію неявною. Інформація про те, для чого використовується змінна, видаляється з її імені, де вона є явною, і підштовхується до її використання, яке є неявним.
dan_waterworth

17

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

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

Отже, від низької виразної сили до високої виразної сили, циклічні конструкції складаються таким чином:

  • Рекурсивні функції хвоста, які використовують незмінні дані,
  • Рекурсивні функції, що використовують незмінні дані,
  • Хоча петлі, які використовують змінні дані,
  • Рекурсивні функції хвоста, які використовують зміни даних,
  • Рекурсивні функції, що використовують зміни даних,

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


1
"цикл у той час, як еквівалент хвостової рекурсивної функції, і рекурсивні функції не повинні бути рекурсивними хвостовими": +1. Ви можете імітувати рекурсію за допомогою циклу "time" + стек.
Джорджо

1
Я не впевнений, що я згоден на 100%, але це, безумовно, цікава перспектива, тому +1 для цього.
Конрад Рудольф

+1 за чудову відповідь, а також за те, що деякі мови (або компілятори) не проводять оптимізацію хвостових викликів.
Дракон Шиван

@ Giorgio, "Ви можете імітувати рекурсію за допомогою циклу" time "+ стек". Тому я сказав експресивну силу. Обчислено, вони однаково потужні.
dan_waterworth

@dan_waterworth: Саме так, як ви сказали у своїй відповіді, рекурсія сама по собі є більш виразною, ніж цикл часу, тому що вам потрібно додати стек до циклу, щоб імітувати рекурсію.
Джорджіо

7

Рекурсія часто менш очевидна. Менш очевидною важче підтримувати.

Якщо ви пишете for(i=0;i<ITER_LIMIT;i++){somefunction(i);}в основному потоці, ви даєте зрозуміти, що ви пишете цикл. Якщо ви пишете, somefunction(ITER_LIMIT);ви не дуже даєте зрозуміти, що буде. Тільки бачачи вміст: цей somefunction(int x)виклик somefunction(x-1)говорить вам, що насправді це цикл із використанням ітерацій. Крім того, ви не можете легко поставити умову втечі з break;десь на півдорозі ітерацій, ви повинні або додати умовний, який буде переданий увесь шлях назад, або викинути виняток. (а винятки знову додають складності ...)

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

Звичайно, якщо це заощадить 98 рядків, це зовсім інша справа.

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

По суті, якщо somefunction(x-1)потрібно викликати всередині себе більше одного разу на рівні, забудьте про повторення.

... Написання ітераційних функцій для завдань, які найкраще виконувати за допомогою рекурсії, можливо, але не приємно. Де б ви не використовували int, вам потрібно щось подібне stack<int>. Я робив це один раз, більше як вправа, ніж для практичних цілей. Можу запевнити, що коли ви зіткнетеся з таким завданням, у вас не виникне сумнівів, як у тих, які ви висловили.


10
Що очевидно, а що менш очевидно, частково залежить від того, до чого ви звикли. Ітерація частіше використовується в мовах програмування, оскільки вона наближається до способу роботи процесора (тобто вона використовує менше пам'яті та виконує швидше). Але якщо ви звикли думати індуктивно, рекурсія може бути настільки ж інтуїтивно зрозумілою.
Джорджіо

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

3
@SF: Так, але ви можете бачити це лише в тому випадку, якщо ви подивитеся на тіло функції. У разі циклу ви бачите цикл, у разі рекурсії ви бачите, що функція викликає себе.
Джорджіо

5
@SF: Схоже, кругові міркування мені: "Якщо в моїй інтуїції це петля, то це цикл". mapможна визначити як рекурсивну функцію (див., наприклад, haskell.org/tutorial/functions.html ), навіть якщо інтуїтивно зрозуміло, що вона переходить список і застосовує функцію до кожного члена списку.
Джорджіо

5
@SF, mapце не ключове слово, це звичайна функція, але це трохи не має значення. Коли функціональні програмісти використовують рекурсію, це зазвичай не тому, що вони хочуть виконати послідовність дій, це тому, що вирішувана проблема може бути виражена функцією та переліком аргументів. Потім проблема може бути зведена до іншої функції та іншого списку аргументів. Зрештою у вас є проблема, яку можна вирішити тривіально.
dan_waterworth

6

Як правило, це взагалі невідчутно, оскільки існують додаткові фактори, які на практиці широко нерівні між випадками і нерівні один для одного у випадку використання. Ось деякі тиски.

  • Короткий, елегантний код загалом перевершує довгий, хитромудрий код.
  • Однак останній пункт дещо визнається недійсним, якщо база розробників не знайома з рекурсією та не бажає / не може вчитися. Він може навіть стати легким негативом, а не позитивним.
  • Рекурсія може бути поганою для ефективності, якщо вам знадобляться глибоко вкладені дзвінки на практиці і ви не можете використовувати хвостову рекурсію (або ваше оточення не може оптимізувати хвостову рекурсію).
  • Рекурсія також є поганою у багатьох випадках, якщо ви не можете правильно кешувати проміжні результати. Наприклад, поширений приклад використання рекурсії дерева для обчислення чисел Фібоначчі виявляється жахливо поганим, якщо ви не кешуєте. Якщо ви кешуєте, це просто, швидко, елегантно і взагалі чудово.
  • Рекурсія в деяких випадках не застосовується, так само як ітерація в інших, і абсолютно необхідна в інших. Прокладка через довгі ланцюги правил бізнесу зазвичай не допомагає рекурсії. Ітерація через потоки даних може бути корисно виконана з рекурсією. Ітерація над багатовимірними динамічними структурами даних (наприклад, лабіринти, дерева об'єктів ...) майже неможлива без рекурсії, явної чи неявної. Зауважте, що в цих випадках явна рекурсія набагато краще, ніж неявна - ніщо не болючіше, ніж читання коду, коли хтось реалізував власний спеціальний, неповний, баггі-стек всередині мови, щоб уникнути страшного R-слова.

Що ви маєте на увазі під кешуванням стосовно рекурсії?
Джорджіо

@ Джорджіо, мабуть, запам'ятовування
jk.

Feh. Якщо ваше оточення не оптимізує хвостові дзвінки, ви повинні знайти кращу середу, а якщо ваші розробники не знайомі з рекурсією, ви повинні знайти кращих розробників. Майте якісь стандарти, люди!
CA McCann

1

Рекурсія - це повтор виклику до функції, цикл - це повторний стрибок, який потрібно розмістити в пам'яті.

Слід згадати також про переповнення стека - http://en.wikipedia.org/wiki/Stack_overflow


1
Рекурсія означає функцію, визначення якої тягне за собою виклик.
hardmath

1
Хоча ваше просте визначення не є точно 100% точним, ви єдиний, хто згадав стек переповнюється.
Qix

1

Це дійсно залежить від зручності чи вимоги:

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

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


3
Ось блог Гвідо ван Россума про те, чому він не хоче оптимізації хвостової рекурсії в Python (підтримуючи думку про те, що різні мови застосовують різні тактичні підходи).
хардмат

-1

Використовуйте схему розробки стратегії.

  • Рекурсія чиста
  • Петлі (мабуть) ефективні

Залежно від вашого навантаження (та / або інших умов), виберіть його.


5
Чекати, що? Як тут вписується схема стратегії? І друге речення звучить як порожня фраза.
Конрад Рудольф

@KonradRudolph Я б пішов на рекурсію. Для дуже великих наборів даних я б перейшов до циклів. Це я мав на увазі. Прошу вибачення, якщо це було не ясно.
Pravin Sonawane

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

@KonradRudolph засвоїв важливе заняття. Глибоко поясніть, що ви хочете сказати .. Дякую .. що допомогло .. :)
Pravin Sonawane

2
@ Pravin Sonawane: Якщо ви можете використовувати оптимізацію хвостової рекурсії, ви можете використовувати рекурсію також на величезних наборах даних.
Джорджіо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.