Рекурсія чи ітерація?


226

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


4
@Warrior Не завжди. Наприклад, з шаховими програмами легше читати рекурсії. "Ітеративна" версія шахового коду насправді не допоможе швидкості та може ускладнити її.
Mateen Ulhaq

12
Чому слід віддати перевагу молоту над пилкою? Викрутка над шилом? Долото над шнеком?
Уейн Конрад

3
Фаворитів немає. Всі вони просто інструменти, кожен зі своїм призначенням. Я б запитав: "У яких проблемах краще ітерація, ніж рекурсія, і навпаки?"
Уейн Конрад

9
"Що так добре в рекурсії?" ... Це рекурсивно саме це. ; o)
Кенг

9
Неправдиве приміщення. Рекурсія не є доброю; насправді це дуже погано. Кожен, хто пише надійне програмне забезпечення, намагатиметься усунути всі рекурсії, оскільки, якщо це не буде оптимізовано заклик хвоста або кількість рівнів, обмежених логарифмічно чи подібними, рекурсія майже завжди призводить до переповнення стека поганого виду.
R .. GitHub СТОП ДОПОМОГА ВІД

Відповіді:


181

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

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


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

15
re: tail recursion is optimized by compilersАле не всі компілятори підтримують хвостову рекурсію ..
Кевін Мередіт

347

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


3
@LeighCaldwell: Я думаю, це точно підводить моє мислення. Шкода всемогутнього не змінився. Я, звичайно, є. :)
Ande Turner

35
Чи знали ви, що вас цитували в книзі через фразу вашої відповіді? LOL amazon.com/Grokking-Algorithms-illustrated-programmers-curious/…
Aipi

4
Мені подобається ця відповідь .. і мені подобається книга "Горкітні алгоритми")
Макс

тож принаймні я та 341 людина читали книгу Алгоритмів погруддя!
zzfima

78

Порівнювати рекурсію з ітерацією подібно порівнянню головки викрутки філіпса з викруткою з плоскою головкою. Здебільшого ви могли зняти будь-який головний гвинт філіпса з плоскою головкою, але було б просто простіше, якби ви використовували викрутку, призначену для цього гвинта?

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

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

Цього має бути достатньо для початку роботи. Я викопаю кілька статей та прикладів і для вас.

Посилання 1: Хаскель проти PHP (Рекурсія проти Ітерації)

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

http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html

Посилання 2: Освоєння рекурсії

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

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

https://developer.ibm.com/articles/l-recurs/

Посилання 3: Чи рекурсія завжди швидша, ніж циклічне? (Відповідь)

Ось посилання на відповідь на запитання про stackoverflow, подібне до вашого. Автор вказує, що багато орієнтирів, пов’язаних або з повторюваним, або з циклічним циклом, дуже специфічні для мови. Імперативні мови, як правило, швидше використовують цикл і повільніше з рекурсією та навпаки для функціональних мов. Думаю, головне з цього посилання - це те, що дуже важко відповісти на питання в мовному агностичному / ситуаційному сенсі.

Чи є рекурсія швидшою, ніж циклічне?


4
Дуже сподобалася аналогія викрутки
jh314


16

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

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


5
Якщо, звичайно, ваш компілятор не оптимізує хвостові дзвінки, як Scala.
Бен Харді

11

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

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

Крім «крайових» випадків (обчислення високої продуктивності, дуже велика глибина рекурсії тощо), бажано скористатися підходом, який найбільш чітко виражає ваші наміри, є добре розробленим та реконструйованим. Оптимізуйте лише після виявлення потреби.


8

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

Наприклад, щоб зробити рекурсивний алгоритм Fibonnaci, ви розбиваєте fib (n) на fib (n-1) і fib (n-2) і обчислюєте обидві частини. Ітерація дозволяє лише повторювати одну функцію знову і знову.

Однак Фібоначчі - насправді зламаний приклад, і я думаю, що ітерація насправді є більш ефективною. Зауважте, що fib (n) = fib (n-1) + fib (n-2) і fib (n-1) = fib (n-2) + fib (n-3). fib (n-1) обчислюється вдвічі!

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

Отже, так - рекурсія краще, ніж ітерація проблем, які можуть бути розбиті на кілька, менших, незалежних, подібних проблем.


1
Обчислення вдвічі вдалося уникнути шляхом запам'ятовування.
Сіддхартха

7

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

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


Це не завжди так. Рекурсія може бути настільки ж ефективною, як ітерація в деяких випадках, коли можна здійснити оптимізацію хвостових викликів. stackoverflow.com/questions/310974/…
Sid Kshatriya

6

Я вважаю, хвостова рекурсія в Java зараз не оптимізована. Деталі викладаються під час цієї дискусії на LtU та пов'язаних з ними посиланнях. Це може бути особливістю у майбутній версії 7, але, мабуть, це створює певні труднощі в поєднанні зі стеком перевірки, оскільки певні кадри відсутні. Інспекція стека застосовується для реалізації їх тонкої зернистої моделі безпеки з Java 2.

http://lambda-the-ultimate.org/node/1333


Для Java є JVM, які оптимізують рекурсію хвоста. ibm.com/developerworks/java/library/j-diag8.html
Ліран Ореві

5

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


5

Рекурсія дуже корисна в деяких ситуаціях. Наприклад, розгляньте код для пошуку факторіалу

int factorial ( int input )
{
  int x, fact = 1;
  for ( x = input; x > 1; x--)
     fact *= x;
  return fact;
}

Тепер розглянемо це за допомогою рекурсивної функції

int factorial ( int input )
{
  if (input == 0)
  {
     return 1;
  }
  return input * factorial(input - 1);
}

Спостерігаючи за цими двома, ми можемо побачити, що рекурсію легко зрозуміти. Але якщо його не використовувати обережно, може бути і стільки схильних помилок. Припустимо, якщо ми пропустимо if (input == 0), то код буде виконуватися деякий час і закінчується, як правило, переповненням стека.


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

@Maxpm, рекурсивне рішення високого порядку набагато краще:, foldl (*) 1 [1..n]це все.
SK-логіка

5

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

Ітеративна реалізація

public static void sort(Comparable[] a)
{
    int N = a.length;
    aux = new Comparable[N];
    for (int sz = 1; sz < N; sz = sz+sz)
        for (int lo = 0; lo < N-sz; lo += sz+sz)
            merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}

Рекурсивна реалізація

private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
    if (hi <= lo) return;
    int mid = lo + (hi - lo) / 2;
    sort(a, aux, lo, mid);
    sort(a, aux, mid+1, hi);
    merge(a, aux, lo, mid, hi);
}

PS - це те, що розповів професор Кевін Уейн (Університет Принстона) на курсі з алгоритмів, представлених на Coursera.


4

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


1
Насправді, складена хвостово-рекурсивна функція Scala зводиться до циклу в байт-коді, якщо ви хочете переглянути їх (рекомендується). Немає накладних викликів функцій. По-друге, хвостово-рекурсивні функції мають перевагу в тому, що вони не потребують змінних змінних / побічних ефектів або явних циклів, що робить коректність набагато простішою.
Бен Харді

4

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


4

Це залежить від мови. У Java слід використовувати петлі. Функціональні мови оптимізують рекурсію.


3

Якщо ви просто повторюєте список, то переконайтесь, що повторіть.

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

Ознайомтеся з методами "знайти" тут: http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html


3

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

Щодо коду - ні, рекурсивний код насправді зрозуміти та підтримувати набагато простіше, ніж суто ітеративний, оскільки більшість структур даних є рекурсивними. Звичайно, для того, щоб виправити це, потрібна мова з підтримкою функцій та закриттів високого порядку, принаймні - акуратно отримати всі стандартні комбінатори та ітератори. Звичайно, у C ++ складні рекурсивні рішення можуть виглядати трохи некрасиво, якщо ви не є хардкор-користувачем FC ++ тощо.


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

2

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


2

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

Наприклад, обчислення класичного факторіалу рекурсивним способом є дуже неефективним через: - ризик переповнення даних - ризик переповнення стека - накладні виклики функцій займають 80% часу виконання

при розробці алгоритму min-max для аналізу позицій в шаховій грі, який буде аналізувати наступні N рухів, можна реалізувати в рекурсії на "глибині аналізу" (як я це роблю ^ _ ^)


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

2

Рекурсія? З чого я розпочну, wiki скаже вам "це процес повторення предметів подібним чином"

Ще в той час, коли я робив C, рекурсія С ++ була богом відправлення, такі речі, як "Хвоста рекурсія". Ви також знайдете багато алгоритмів сортування, що використовують рекурсію. Швидкий приклад сортування: http://alienryderflex.com/quicksort/

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


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

Справедливий пункт, це було назад. Однак я не впевнений, що це все ще застосовно для хвостової рекурсії.
Nickz

2

У C ++, якщо рекурсивна функція є шаблонною, у компілятора є більше шансів її оптимізувати, оскільки всі дедукції типу та екземпляри функцій будуть відбуватися за час компіляції. Сучасні компілятори також можуть вбудувати функцію, якщо це можливо. Отже, якщо ви використовуєте прапори оптимізації, як-от -O3або -O2в них g++, то рекурсії можуть мати шанс бути швидшими за ітерації. У ітеративних кодах компілятор отримує менше шансів оптимізувати його, оскільки він вже знаходиться у більш-менш оптимальному стані (якщо він написаний досить добре).

У моєму випадку я намагався реалізувати матричну експоненцію шляхом вирівнювання за допомогою матричних об'єктів Armadillo як в рекурсивному, так і в ітеративному способі. Алгоритм можна знайти тут ... https://en.wikipedia.org/wiki/Exponentiation_by_squaring . Мої функції були шаблонні, і я обчислював 1,000,000 12x12матриці, підняті до потужності 10. Я отримав такий результат:

iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec

iterative + No-optimisation flag  -> 2.83.. sec
recursive + No-optimisation flag  -> 4.15.. sec

Ці результати були отримані за допомогою gcc-4.8 з прапорцем c ++ 11 ( -std=c++11) та Armadillo 6.1 з Intel mkl. Компілятор Intel також показує подібні результати.


1

Майк прав. Рекурсія хвоста не оптимізована компілятором Java або JVM. Ви завжди отримаєте переповнення стека чимось таким:

int count(int i) {
  return i >= 100000000 ? i : count(i+1);
}

3
Якщо ви не пишете це на Scala ;-)
Бен Харді

1

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


1

Рекурсія має недолік у тому, що алгоритм, який ви пишете, використовуючи рекурсію, має складність простору O (n). Хоча ітеративний підхід має просторову складність O (1). Це є перевагою використання ітерації над рекурсією. Тоді для чого ми використовуємо рекурсію?

Дивись нижче.

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


1

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

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

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


0

Наскільки я знаю, Perl не оптимізує хвостові рекурсивні дзвінки, але ви можете підробити це.

sub f{
  my($l,$r) = @_;

  if( $l >= $r ){
    return $l;
  } else {

    # return f( $l+1, $r );

    @_ = ( $l+1, $r );
    goto &f;

  }
}

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

Зауважте, що немає " my @_;" або " local @_;", якщо ви це зробили, це більше не працюватиме.


0

Використовуючи лише Chrome 45.0.2454.85 м, здається, що рекурсія на приємніший показник швидша.

Ось код:

(function recursionVsForLoop(global) {
    "use strict";

    // Perf test
    function perfTest() {}

    perfTest.prototype.do = function(ns, fn) {
        console.time(ns);
        fn();
        console.timeEnd(ns);
    };

    // Recursion method
    (function recur() {
        var count = 0;
        global.recurFn = function recurFn(fn, cycles) {
            fn();
            count = count + 1;
            if (count !== cycles) recurFn(fn, cycles);
        };
    })();

    // Looped method
    function loopFn(fn, cycles) {
        for (var i = 0; i < cycles; i++) {
            fn();
        }
    }

    // Tests
    var curTest = new perfTest(),
        testsToRun = 100;

    curTest.do('recursion', function() {
        recurFn(function() {
            console.log('a recur run.');
        }, testsToRun);
    });

    curTest.do('loop', function() {
        loopFn(function() {
            console.log('a loop run.');
        }, testsToRun);
    });

})(window);

РЕЗУЛЬТАТИ

// 100 запускається, використовуючи стандарт для циклу

100x для циклу. Час завершення: 7,683ms

// 100 запускається з використанням функціонально-рекурсивного підходу з рекурсією хвоста

100-разовий пробіг рекурсії. Час закінчення: 4.841ms

На наведеному нижче скріншоті рекурсія знову виграє з більшим відривом при виконанні 300 циклів за тест

Рекурсія знову перемагає!


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

0

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

Коротше кажучи: 1) ітераційне проходження після замовлення непросте - це робить DFT складнішим 2) перевірка циклів простіше з рекурсією

Деталі:

У рекурсивному випадку легко створювати попередні та після траверси:

Уявіть собі досить стандартне запитання: "роздрукуйте всі завдання, які слід виконати для виконання завдання 5, коли завдання залежать від інших завдань"

Приклад:

    //key-task, value-list of tasks the key task depends on
    //"adjacency map":
    Map<Integer, List<Integer>> tasksMap = new HashMap<>();
    tasksMap.put(0, new ArrayList<>());
    tasksMap.put(1, new ArrayList<>());

    List<Integer> t2 = new ArrayList<>();
    t2.add(0);
    t2.add(1);
    tasksMap.put(2, t2);

    List<Integer> t3 = new ArrayList<>();
    t3.add(2);
    t3.add(10);
    tasksMap.put(3, t3);

    List<Integer> t4 = new ArrayList<>();
    t4.add(3);
    tasksMap.put(4, t4);

    List<Integer> t5 = new ArrayList<>();
    t5.add(3);
    tasksMap.put(5, t5);

    tasksMap.put(6, new ArrayList<>());
    tasksMap.put(7, new ArrayList<>());

    List<Integer> t8 = new ArrayList<>();
    t8.add(5);
    tasksMap.put(8, t8);

    List<Integer> t9 = new ArrayList<>();
    t9.add(4);
    tasksMap.put(9, t9);

    tasksMap.put(10, new ArrayList<>());

    //task to analyze:
    int task = 5;


    List<Integer> res11 = getTasksInOrderDftReqPostOrder(tasksMap, task);
    System.out.println(res11);**//note, no reverse required**

    List<Integer> res12 = getTasksInOrderDftReqPreOrder(tasksMap, task);
    Collections.reverse(res12);//note reverse!
    System.out.println(res12);

    private static List<Integer> getTasksInOrderDftReqPreOrder(Map<Integer, List<Integer>> tasksMap, int task) {
         List<Integer> result = new ArrayList<>();
         Set<Integer> visited = new HashSet<>();
         reqPreOrder(tasksMap,task,result, visited);
         return result;
    }

private static void reqPreOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {

    if(!visited.contains(task)) {
        visited.add(task);
        result.add(task);//pre order!
        List<Integer> children = tasksMap.get(task);
        if (children != null && children.size() > 0) {
            for (Integer child : children) {
                reqPreOrder(tasksMap,child,result, visited);
            }
        }
    }
}

private static List<Integer> getTasksInOrderDftReqPostOrder(Map<Integer, List<Integer>> tasksMap, int task) {
    List<Integer> result = new ArrayList<>();
    Set<Integer> visited = new HashSet<>();
    reqPostOrder(tasksMap,task,result, visited);
    return result;
}

private static void reqPostOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
    if(!visited.contains(task)) {
        visited.add(task);
        List<Integer> children = tasksMap.get(task);
        if (children != null && children.size() > 0) {
            for (Integer child : children) {
                reqPostOrder(tasksMap,child,result, visited);
            }
        }
        result.add(task);//post order!
    }
}

Зауважимо, що рекурсивне відстеження після замовлення не потребує подальшого звороту результату. Діти надрукували перше і ваше завдання у запитанні надрукували останнім. Все добре. Ви можете виконати рекурсивний перегляд попереднього замовлення (також показано вище), і для цього знадобиться сторнувати список результатів.

Не так просто з ітераційним підходом! При ітеративному (один стек) підхід ви можете зробити лише попереднє замовлення-обхід, тому ви зобов'язані повернути результат масиву в кінці:

    List<Integer> res1 = getTasksInOrderDftStack(tasksMap, task);
    Collections.reverse(res1);//note reverse!
    System.out.println(res1);

    private static List<Integer> getTasksInOrderDftStack(Map<Integer, List<Integer>> tasksMap, int task) {
    List<Integer> result = new ArrayList<>();
    Set<Integer> visited = new HashSet<>();
    Stack<Integer> st = new Stack<>();


    st.add(task);
    visited.add(task);

    while(!st.isEmpty()){
        Integer node = st.pop();
        List<Integer> children = tasksMap.get(node);
        result.add(node);
        if(children!=null && children.size() > 0){
            for(Integer child:children){
                if(!visited.contains(child)){
                    st.add(child);
                    visited.add(child);
                }
            }
        }
        //If you put it here - it does not matter - it is anyway a pre-order
        //result.add(node);
    }
    return result;
}

Виглядає просто, ні?

Але це пастка в деяких інтерв'ю.

Це означає наступне: за допомогою рекурсивного підходу ви можете реалізувати глибину першої траверси та потім вибрати, яке замовлення вам потрібно попередньо або опублікувати (просто змінивши місце розташування "друку", в нашому випадку "додавання до списку результатів" ). Завдяки ітеративному підходу (один стек) ви можете легко виконати лише попереднє замовлення, і тому в ситуації, коли дітей потрібно надрукувати спочатку (майже всі ситуації, коли вам потрібно почати друкувати з нижніх вузлів, піднімаючись вгору) - ви знаходитесь в біда. Якщо у вас виникли проблеми, ви можете їх змінити пізніше, але це буде доповненням до вашого алгоритму. І якщо інтерв'юер дивиться на годинник, це може бути для вас проблемою. Існують складні способи зробити ітераційне обхід після замовлення, вони існують, але вони є не прості . Приклад:https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/

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

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

Ще один плюс рекурсії - простіше уникнути / помітити цикли в графіку.

Приклад (предокод):

dft(n){
    mark(n)
    for(child: n.children){
        if(marked(child)) 
            explode - cycle found!!!
        dft(child)
    }
    unmark(n)
}

0

Це може бути цікаво писати це як рекурсія або як практика.

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

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

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

Якщо ви зменшуєте розмір даних або nвдвічі щоразу, коли повторюєте, то взагалі вам не потрібно турбуватися про переповнення стека. Скажімо, якщо для складання програми переповнення програмі необхідно мати глибину 4,000 рівня або глибину 10 000, то для розміщення вашої програми розмір даних повинен становити приблизно 2 4000 . З огляду на це, найбільший пристрій зберігання даних останнім часом може вмістити 2 61 байт, а якщо у вас є 2 61 таких пристроїв, ви маєте справу лише з 2 122 розмірами даних. Якщо ви дивитесь на всі атоми у Всесвіті, вважається, що це може бути менше 2 84. Якщо вам потрібно мати справу з усіма даними у Всесвіті та їхніми станами за кожні мілісекунди з моменту народження Всесвіту, за оцінками, 14 мільярдів років тому, це може бути лише 2 153 . Отже, якщо ваша програма може обробляти 2 000 одиниць даних або n, ви можете обробляти всі дані у Всесвіті, і програма не буде стеком переповнювати. Якщо вам не потрібно мати справу з числами величиною до 2 4000 (ціле число 4000), то взагалі вам не потрібно турбуватися про переповнення стека.

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

Тож якщо у вас є можливість переповнення стека, спробуйте зробити це ітераційним рішенням.


-1

Я відповім на ваше запитання, створивши структуру даних Haskell за допомогою "індукції", яка є свого роду "подвійною" для рекурсії. І тоді я покажу, як ця подвійність призводить до приємних речей.

Введемо тип для простого дерева:

data Tree a = Branch (Tree a) (Tree a)
            | Leaf a
            deriving (Eq)

Ми можемо прочитати це визначення, як сказати "Дерево - це гілка (яка містить два дерева) або є листочком (який містить значення даних)". Тож листок - це свого роду мінімальний випадок. Якщо дерево не є листям, то воно повинно бути складеним деревом, що містить два дерева. Це єдині випадки.

Давайте зробимо дерево:

example :: Tree Int
example = Branch (Leaf 1) 
                 (Branch (Leaf 2) 
                         (Leaf 3))

Тепер, припустимо, ми хочемо додати 1 до кожного значення дерева. Це можна зробити, зателефонувавши:

addOne :: Tree Int -> Tree Int
addOne (Branch a b) = Branch (addOne a) (addOne b)
addOne (Leaf a)     = Leaf (a + 1)

По-перше, зауважте, що це насправді рекурсивне визначення. Він сприймає конструктори даних Branch і Leaf як випадки (а оскільки Leaf мінімальний і це єдині можливі випадки), ми впевнені, що функція припиняється.

Що потрібно для написання addOne в ітеративному стилі? Як виглядатиме циклічне утворення довільної кількості гілок?

Крім того, подібний тип рекурсії часто може бути врахований з точки зору "функтора". Ми можемо перетворити Дерева в Функтори, визначивши:

instance Functor Tree where fmap f (Leaf a)     = Leaf (f a)
                            fmap f (Branch a b) = Branch (fmap f a) (fmap f b)

та визначаючи:

addOne' = fmap (+1)

Ми можемо визначити інші схеми рекурсії, такі як катаморфізм (або складка) для алгебраїчного типу даних. Використовуючи катаморфізм, ми можемо написати:

addOne'' = cata go where
           go (Leaf a) = Leaf (a + 1)
           go (Branch a b) = Branch a b

-2

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

Рекурсія - це спосіб пройти, якщо ви хочете повторити файли, впевнені, що так 'знайти * | "grep *" працює. Своєрідна подвійна рекурсія, особливо з трубою (але не робіть купу систематичних дзвінків, як так багато хто любить робити, якщо це все, що ви збираєтеся поставити там для використання іншими).

Мови вищого рівня та навіть clang / cpp можуть реалізувати його однаково у фоновому режимі.


1
"Переповнення стека буде мати місце, якщо ви програмуєте мовою, яка не має вбудованого управління пам'яттю" - не має сенсу. Більшість мов використовують стек обмеженого розміру, тому рекурсія призведе до збою досить скоро.
StaceyGirl
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.