Розуміння того, як працюють рекурсивні функції


115

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

Розуміючи, що зіткнувшись з тим, що ми не знаємо того, чого не знаємо, ми можемо схильно ставити неправильні запитання або неправильно ставити правильні запитання, я поділюсь тим, про що я «думаю», моє питання полягає у надії, що хтось із подібним світоглядом може поділитися деяким трохи знань, які допоможуть увімкнути рекурсивну лампочку для мене!

Ось функція (синтаксис записаний у Swift):

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a: a + 1, b: b)
    }
}

Ми будемо використовувати 2 і 5 як наші аргументи:

println(sumInts(a: 2, b: 5))

Очевидно, що відповідь - 14. Але мені не зрозуміло, як це значення досягається.

Це мої 2 повішання:

  1. Функція викликається рекурсивно, поки не буде виконано умову. Ця умова є a> b. Коли ця умова буде виконана, поверніть 0. На перший погляд я б очікував, що значення повернення буде 0, що, очевидно, неправильно.

  2. Виведення значення "a" на кожну ітерацію дає значення, яке я б очікував: 2, 3, 4, 5 (в якому пункт 5 + 1> b, що відповідає першій умові: a> b), але я все одно не став " t дивіться, як досягається значення 14.

Моя перша думка полягає в тому, що щось подібне до цього відбувається чарівно:

var answer = a;
answer += a+1 until a > b;
return answer;   

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

Якби хтось міг люб’язно пояснити, що технічно відбувається під час подібних функцій і чому результат не 0, і як, зрештою a + sumInts(a: a + 1, b: b) = 14, я б назавжди був у твоїх боргах.


7
Рекурсія - одне з тих концепцій програмування, які набагато простіше зрозуміти в математичному плані, ніж у коді; є визначення добре тут
blgt

5
LearnYouARecursion, повний набір проблем від професора світового рівня!
recursion.ninja

15
Мені просто потрібно закликати вас ввести "Рекурсію" у вікні пошуку Google. Одне з таких писанок. Я не зіпсую сюрприз для вас.
Флоріс

7
Можливий дублікат stackoverflow.com/questions/25676961/…
Neil McGuigan

Відповіді:


107

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

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

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

return a + sumInts(a + 1, b: b)

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

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

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


7
Catfish_Man: Я думаю, ти це прибив! Думати про нього як про кілька «копій» однієї і тієї ж функції має тотальний сенс. Я все ще обертаю голову навколо цього, але я думаю, що ти відправив мене на правильний шлях! Дякуємо, що знайшли час із напруженого дня, щоб допомогти колезі-програмісту! Я позначу вашу відповідь правильною. Гарного дня!
Джейсон Елвуд

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

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

5
Правильна термінологія полягає в тому, що існує кілька викликів функції. Кожна виклик має власні екземпляри змінних aта b.
Теодор Норвелл

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

130

1. Функція викликається рекурсивно, поки не буде виконано умову. Такий стан є a > b. Коли ця умова буде виконана, поверніть 0. На перший погляд я б очікував, що значення повернення буде 0, що, очевидно, неправильно.

Ось що sumInts(2,5)б подумали комп'ютерні обчислення , якби вони змогли:

I want to compute sumInts(2, 5)
for this, I need to compute sumInts(3, 5)
and add 2 to the result.
  I want to compute sumInts(3, 5)
  for this, I need to compute sumInts(4, 5)
  and add 3 to the result.
    I want to compute sumInts(4, 5)
    for this, I need to compute sumInts(5, 5)
    and add 4 to the result.
      I want to compute sumInts(5, 5)
      for this, I need to compute sumInts(6, 5)
      and add 5 to the result.
        I want to compute sumInts(6, 5)
        since 6 > 5, this is zero.
      The computation yielded 0, therefore I shall return 5 = 5 + 0.
    The computation yielded 5, therefore I shall return 9 = 4 + 5.
  The computation yielded 9, therefore I shall return 12 = 3 + 9.
The computation yielded 12, therefore I shall return 14 = 2 + 12.

Як бачите, деякий виклик функції sumIntsфактично повертає 0, однак це не остаточне значення, оскільки комп'ютер все одно повинен додати 5 до 0, потім 4 до результату, потім 3, потім 2, як описано чотирма останніми реченнями думки нашого комп’ютера. Зауважте, що в ході рекурсії комп'ютер не тільки повинен обчислювати рекурсивний виклик, він також повинен пам'ятати, що робити зі значенням, поверненим рекурсивним викликом. Існує особлива область пам’яті комп’ютера, яка називається стеком, де зберігається така інформація, цей простір обмежений, і функції, які є занадто рекурсивними, можуть вичерпати стек: це переповнення стека, що дає свою назву нашому найбільш улюбленому веб-сайту.

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

2. Вивівши значення "a" на кожній ітерації, ви отримаєте значення, яке я б очікував: 2, 3, 4, 5 (в якому пункт 5 + 1> b, що відповідає першій умові: a> b), але я все одно не бачите, як досягається значення 14.

Це відбувається тому, що повернене значення - це не aсаме по собі, а сума значення aта значення, повернене рекурсивним викликом.


3
Дякуємо, що знайшли час, щоб написати цю чудову відповідь Майкл! +1!
Джейсон Елвуд

9
@JasonElwood Можливо, це корисно, якщо ви модифікуєте sumIntsтак, щоб вони насправді записували "комп'ютерні думки". Після того, як ви написали руку таких функцій, ви, мабуть, отримали її!
Майкл Ле Барб'є Грюневальд,

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

1
@EricLippert Хоча методи, що використовуються для впровадження рекурсивності, є цікавою темою сама по собі , я не впевнений, чи було б корисним для ОП - хто хоче зрозуміти "як це працює" - щоб зазнати різноманітності використовуваних механізмів. У той час як продовження проходження стилю або мов , на основі розширення (наприклад , TeX і m4) по своїй суті не складніше , ніж більш загальні парадигми програмування, я не буду правопорушенням кого з маркування цих «екзотичного» і трохи білої брехні , як «це завжди відбувається в стеці» повинен допомогти ОП зрозуміти концепцію. (І своєрідний стек завжди задіяний.)
Майкл Ле Барб'є Грюневальд

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

48

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

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

Дозвольте показати вам кроки:

sumInts(a: 2, b: 5) will return: 2 + sumInts(a: 3, b: 5)
sumInts(a: 3, b: 5) will return: 3 + sumInts(a: 4, b: 5)
sumInts(a: 4, b: 5) will return: 4 + sumInts(a: 5, b: 5)
sumInts(a: 5, b: 5) will return: 5 + sumInts(a: 6, b: 5)
sumInts(a: 6, b: 5) will return: 0

як тільки sumInts (a: 6, b: 5) виконано, результати можна обчислити, таким чином повертаючи назад ланцюг з отриманими результатами:

 sumInts(a: 6, b: 5) = 0
 sumInts(a: 5, b: 5) = 5 + 0 = 5
 sumInts(a: 4, b: 5) = 4 + 5 = 9
 sumInts(a: 3, b: 5) = 3 + 9 = 12
 sumInts(a: 2, b: 5) = 2 + 12 = 14.

Ще один спосіб представити структуру рекурсії:

 sumInts(a: 2, b: 5) = 2 + sumInts(a: 3, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + sumInts(a: 4, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + sumInts(a: 5, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + sumInts(a: 6, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + 0
 sumInts(a: 2, b: 5) = 14 

2
Дуже добре, Роб. Ви поставили це так, що це дуже зрозуміло і легко зрозуміти. Дякуємо, що знайшли час!
Джейсон Елвуд

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

2
Я радий. :) Пояснити ці речі не завжди просто. Дякую вам за комплімент.
Роб

1
+1. Ось як я би це описав, конкретно з вашим останнім прикладом структури. Корисно візуально розгорнути те, що відбувається.
KChaloux

40

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

Код, який ви вказали тут, вирішує таку проблему: ви хочете знати суму всіх цілих чисел від a до b, включно. Для вашого прикладу ви хочете суму чисел від 2 до 5, включно, що є

2 + 3 + 4 + 5

Під час спроби вирішити проблему рекурсивно, одним із перших кроків має бути з'ясування, як розбити проблему на меншу проблему з тією ж структурою. Тож припустимо, що ви хотіли підсумувати числа від 2 до 5 включно. Один із способів спростити це - помітити, що вищевказану суму можна переписати як

2 + (3 + 4 + 5)

Тут (3 + 4 + 5) трапляється сума всіх цілих чисел між 3 і 5 включно. Іншими словами, якщо ви хочете дізнатися суму всіх цілих чисел між 2 і 5, почніть з обчислення суми всіх цілих чисел між 3 і 5, а потім додайте 2.

Тож як ви обчислити суму всіх цілих чисел між 3 і 5 включно? Ну, така сума є

3 + 4 + 5

що можна вважати замість цього

3 + (4 + 5)

Тут (4 + 5) - сума всіх цілих чисел між 4 і 5 включно. Отже, якщо ви хочете обчислити суму всіх чисел від 3 до 5, включно, ви обчислите суму всіх цілих чисел між 4 і 5, а потім додасте 3.

Тут є візерунок! Якщо ви хочете обчислити суму цілих чисел між a і b, включно, ви можете зробити наступне. Спочатку обчисліть суму цілих чисел між + 1 та b включно. Далі додайте до цього загального значення. Ви помітите, що "обчислити суму цілих чисел між + 1 і b, включно", трапляється майже однакова проблема, яку ми вже намагаємося вирішити, але з дещо іншими параметрами. Замість того, щоб обчислити від a до b, включно, ми обчислюємо від + 1 до b включно. Це рекурсивний крок - щоб вирішити більшу задачу ("сума від a до b, включно"), ми зменшуємо проблему до меншої версії себе ("сума від a + 1 до b, включно.").

Якщо ви подивитесь на вищезазначений код, ви помітите, що в ньому є цей крок:

return a + sumInts(a + 1, b: b)

Цей код є просто перекладом наведеної вище логіки - якщо ви хочете підсумовувати від a до b, включно, почніть з підсумовування а + 1 до b, включно (це рекурсивний виклик sumInts), а потім додайте a.

Звичайно, сам по собі такий підхід насправді не спрацює. Наприклад, як би ви обчислили суму всіх цілих чисел від 5 до 5 включно? Ну, використовуючи нашу нинішню логіку, ви обчислите суму всіх цілих чисел між 6 і 5, включно, а потім додасте 5. Отже, як обчислити суму всіх цілих чисел між 6 і 5 включно? Ну, використовуючи нашу нинішню логіку, ви обчислите суму всіх цілих чисел між 7 і 5 включно, а потім додасте 6. Ви помітите тут проблему - це просто продовжує йти!

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

Отже, що є базовим у цій конкретній проблемі? Коли ви підсумовуєте цілі числа від a до b, включно, якщо випадково більший за b, то відповідь - 0 - в діапазоні немає жодних чисел! Тому ми будемо структурувати наше рішення так:

  1. Якщо a> b, то відповідь - 0.
  2. В іншому випадку (a ≤ b) отримайте відповідь так:
    1. Обчисліть суму цілих чисел між a + 1 і b.
    2. Додайте відповідь, щоб отримати відповідь.

Тепер порівняйте цей псевдокод із фактичним кодом:

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b: b)
    }
}

Зверніть увагу, що між рішенням, викладеним у псевдокоді, і цим фактичним кодом майже майже одна карта. Перший крок - базовий випадок - якщо ви запитаєте суму порожнього діапазону чисел, ви отримуєте 0. В іншому випадку обчисліть суму між а + 1 і b, а потім перейдіть, додайте а.

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

Спробуємо дуже-дуже простий випадок. Що станеться, якщо ви телефонуєте sumInts(6, 5)? У цьому випадку, простежуючи код, ви бачите, що функція просто повертає 0. Це правильно зробити, щоб - в діапазоні немає жодних чисел. А тепер спробуйте щось складніше. Що відбувається, коли ви телефонуєте sumInts(5, 5)? Ну ось що відбувається:

  1. Ви телефонуєте sumInts(5, 5). Ми потрапляємо у elseгілку, яка повертає значення `a + sumInts (6, 5).
  2. Для того, sumInts(5, 5)щоб визначити, що sumInts(6, 5)таке, нам потрібно призупинити, що ми робимо, і зателефонувати sumInts(6, 5).
  3. sumInts(6, 5)називається. Він заходить у ifгілку і повертається 0. Однак цей екземпляр sumIntsвикликав sumInts(5, 5), тому повернене значення передається назад sumInts(5, 5), а не абоненту верхнього рівня.
  4. sumInts(5, 5)тепер можна обчислити, 5 + sumInts(6, 5)щоб повернутися назад 5. Потім він повертає його абоненту верхнього рівня.

Зверніть увагу, як тут утворилося значення 5. Ми почали з одного активного дзвінка на sumInts. Це припинило черговий рекурсивний виклик, і значення, повернене цим викликом, передало інформацію назад до sumInts(5, 5). Виклик до sumInts(5, 5)того по черзі робив деякі обчислення і повертав значення, що викликається.

Якщо ви спробуєте це sumInts(4, 5), ось що буде:

  • sumInts(4, 5)намагається повернутися 4 + sumInts(5, 5). Для цього він закликає sumInts(5, 5).
    • sumInts(5, 5)намагається повернутися 5 + sumInts(6, 5). Для цього він закликає sumInts(6, 5).
    • sumInts(6, 5)повертає 0 назад до sumInts(5, 5).</li> <li>sumInts (5, 5) now has a value forsumInts (6, 5) , namely 0. It then returns5 + 0 = 5`.
  • sumInts(4, 5)тепер має значення для sumInts(5, 5), а саме 5. Потім воно повертається 4 + 5 = 9.

Іншими словами, повернене значення формується шляхом підсумовування значень по одному, кожен раз приймаючи одне значення, повернене певним рекурсивним викликом sumIntsі додаючи поточне значення a. Коли рекурсія знижується, найглибший виклик повертається 0. Однак це значення не одразу виходить з ланцюга рекурсивних викликів; натомість він просто повертає значення назад до рекурсивного виклику одного шару над ним. Таким чином, кожен рекурсивний дзвінок просто додає ще одне число і повертає його вище в ланцюжку, що завершується загальним підсумком. Як вправу, спробуйте простежити це, з sumInts(2, 5)чого ви хотіли почати.

Сподіваюся, це допомагає!


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

22

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

По-перше, я написав багато статей про прості рекурсивні алгоритми, які можуть вам бути цікавими; побачити

http://ericlippert.com/tag/recursion/

http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

Вони в найновішому порядку, тому починайте знизу.

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

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

s = (a, b) => a > b ? 0 : a + s(a + 1, b)

Сподіваюся, це має сенс. Якщо ви не знайомі з умовним оператором, він має форму condition ? consequence : alternativeта його значення стане зрозумілим.

Тепер ми хочемо оцінити. s(2,5) Ми робимо це за допомогою текстової заміни дзвінка на тіло функції, а потім замінимо aна 2і bна 5:

s(2, 5) 
---> 2 > 5 ? 0 : 2 + s(2 + 1, 5)

Тепер оцініть умовне. Ми текстуально замінити 2 > 5з false.

---> false ? 0 : 2 + s(2 + 1, 5)

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

---> 2 + s(2 + 1, 5)

Тепер, щоб врятувати мені необхідність набрати всі ці +знаки, текстуально замініть постійну арифметику її значенням. (Це трохи обман, але мені не хочеться слідкувати за всіма дужками!)

---> 2 + s(3, 5)

Тепер шукайте та замінюйте, на цей раз, із тілом для дзвінка, 3для aта 5для b. Ми помістимо заміну дзвінка в дужки:

---> 2 + (3 > 5 ? 0 : 3 + s(3 + 1, 5))

А тепер ми просто продовжуємо робити ті самі кроки підстановки тексту:

---> 2 + (false ? 0 : 3 + s(3 + 1, 5))  
---> 2 + (3 + s(3 + 1, 5))                
---> 2 + (3 + s(4, 5))                     
---> 2 + (3 + (4 > 5 ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (false ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(5, 5)))
---> 2 + (3 + (4 + (5 > 5 ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (false ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(6, 5))))
---> 2 + (3 + (4 + (5 + (6 > 5 ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + (true ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + 0)))
---> 2 + (3 + (4 + 5))
---> 2 + (3 + 9)
---> 2 + 12
---> 14

Все, що ми тут робили, - це просто проста текстова підміна . Дійсно, я не повинен був замінити "3" на "2 + 1" і так далі, поки мені не довелося, але педагогічно це важко було б прочитати.

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

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

То що ж таке безмежна рекурсія? Рекурсія, де підміна тексту не припиняється! Зверніть увагу, як врешті-решт ми дійшли до кроку, коли не можна було більше sзамінити, і тоді ми могли просто застосувати правила для арифметики.


Хороший приклад, але це розбиває ваше серце, коли ви продовжуєте робити більш складні обчислення. Напр. Пошук спільного предка у Бінарному дереві.
CodeYogi

11

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

Спочатку базовий випадок:

sumInts(6, 5) = 0

Тоді виклик трохи вище, ніж у стеку викликів :

sumInts(5, 5) == 5 + sumInts(6, 5)
sumInts(5, 5) == 5 + 0
sumInts(5, 5) == 5

Тоді виклик трохи вище, ніж у стеку викликів:

sumInts(4, 5) == 4 + sumInts(5, 5)
sumInts(4, 5) == 4 + 5
sumInts(4, 5) == 9

І так далі:

sumInts(3, 5) == 3 + sumInts(4, 5)
sumInts(3, 5) == 3 + 9
sumInts(3, 5) == 12

І так далі:

sumInts(2, 5) == 2 + sumInts(3, 5)
sumInts(4, 5) == 2 + 12
sumInts(4, 5) == 14

Зауважте, що ми дійшли до нашого оригінального дзвінка до функції sumInts(2, 5) == 14

Порядок виконання цих дзвінків:

sumInts(2, 5)
sumInts(3, 5)
sumInts(4, 5)
sumInts(5, 5)
sumInts(6, 5)

Порядок повернення цих дзвінків:

sumInts(6, 5)
sumInts(5, 5)
sumInts(4, 5)
sumInts(3, 5)
sumInts(2, 5)

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


5

Я піду.

Виконавши рівняння a + sumInts (a + 1, b), я покажу як остаточна відповідь 14.

//the sumInts function definition
func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b)
    }
}

Given: a = 2 and b = 5

1) 2 + sumInts(2+1, 5)

2) sumInts(3, 5) = 12
   i) 3 + sumInts(3+1, 5)
   ii) 4 + sumInts(4+1, 5)
   iii) 5 + sumInts(5+1, 5)
   iv) return 0
   v) return 5 + 0
   vi) return 4 + 5
   vii) return 3 + 9

3) 2 + 12 = 14.

Повідомте нас, якщо у вас є додаткові запитання.

Ось ще один приклад рекурсивних функцій у наступному прикладі.

Чоловік щойно закінчив коледж.

t - кількість часу в роках.

Загальну фактичну кількість років, відпрацьованих до виходу на пенсію, можна обчислити так:

public class DoIReallyWantToKnow 
{
    public int howLongDoIHaveToWork(int currentAge)
    {
      const int DESIRED_RETIREMENT_AGE = 65;
      double collectedMoney = 0.00; //remember, you just graduated college
      double neededMoneyToRetire = 1000000.00

      t = 0;
      return work(t+1);
    }

    public int work(int time)
    {
      collectedMoney = getCollectedMoney();

      if(currentAge >= DESIRED_RETIREMENT_AGE 
          && collectedMoney == neededMoneyToRetire
      {
        return time;
      }

      return work(time + 1);
    }
}

І цього має бути достатньо, щоб когось пригнічувати, хаха. ;-P


5

Рекурсія. У галузі інформатики рекурсія висвітлена глибоко в темі Кінцевих автоматів.

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

Це може бути інакше, якби твердження було "моя машина - Бентлі. Моя машина синя". У такому випадку заміна в другій ситуації для автомобіля може бути "bentley", в результаті чого "мій bentley синій". Ці типи підстановок математично пояснюються в "Комп'ютерній науці" за допомогою контекстуальних граматик .

Фактична підміна є виробничим правилом. Зважаючи на те, що твердження представлено S і що автомобіль є змінною, яка може бути "bentley", це твердження може бути рекурсивно реконструйовано.

S -> "my"S | " "S | CS | "is"S | "blue"S | ε
C -> "bentley"

Це можна побудувати кількома способами, оскільки кожен |означає, що є вибір. Sможна замінити будь-яким із цих варіантів, і S завжди починається порожнім. В εзасіб припинити виробництво. Так само, як Sможуть бути замінені, так можуть бути і інші змінні (існує лише одна, і вона представляла Cб "bentley").

Тож починаючи з Sтого, щоб бути порожнім, і замінити його на перший вибір "my"S Sстає

"my"S

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

"my "S

Далі давайте вибрати C

"my "CS

І у C є лише один вибір для заміни

"my bentley"S

І простір знову для S

"my bentley "S

І так далі "my bentley is"S, "my bentley is "S, "my bentley is blue"S, "my bentley is blue"(заміна S для е закінчує виробництво) і ми рекурсивно побудували наше твердження «мій Бентлі синій».

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

S -> 2 + A
A -> 3 + B
B -> 4 + C
C -> 5 + D
D -> 0

Це стає

2 + A
2 + 3 + B
2 + 3 + 4 + C
2 + 3 + 4 + 5 + D
2 + 3 + 4 + 5 + 0
14

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

4

Я думаю, що найкращий спосіб зрозуміти рекурсивні функції - це зрозуміти, що вони створені для обробки рекурсивних структур даних. Але у вашій оригінальній функції, sumInts(a: Int, b: Int)яка обчислює рекурсивно суму чисел від aдо b, здається, це не рекурсивна структура даних ... Давайте спробуємо трохи модифіковану версію, sumInts(a: Int, n: Int)де nкількість доданих вами чисел.

Тепер сумиЗ рекурсивні n, натуральне число. Все ще не рекурсивні дані, правда? Ну, природним числом можна вважати рекурсивну структуру даних, використовуючи аксіоми Пеано:

enum Natural = {
    case Zero
    case Successor(Natural)
}

Отже, 0 = Нуль, 1 = Суцесор (Нуль), 2 = Суцесор (Суцесор (Нуль)) тощо.

Коли у вас є рекурсивна структура даних, у вас є шаблон для функції. Для кожного нерекурсивного випадку ви можете обчислити значення безпосередньо. Для рекурсивних випадків ви припускаєте, що рекурсивна функція вже працює, і використовуєте її для обчислення випадку, але деконструюючи аргумент. У випадку з Natural, це означає, що замість цього Succesor(n)ми будемо використовувати n, або рівнозначно, замість того, що nми будемо використовувати n - 1.

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        // non recursive case
    } else {
        // recursive case. We use sumInts(..., n - 1)
    }
}

Тепер рекурсивна функція простіше програмувати. По- перше, базовий випадок, n=0. Що нам повернути, якщо ми не хочемо додати чисел? Відповідь, звичайно, 0.

А як щодо рекурсивного випадку? Якщо ми хочемо додати nчисла, починаючи з, aі у нас вже є робоча sumIntsфункція, яка працює для n-1? Ну, нам потрібно додати aі потім викликати sumIntsз a + 1, так що ми в кінці кінців з:

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        return 0
    } else {
        return a + sumInts(a + 1, n - 1)
    }
}

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

  • Для базових випадків рекурсивних даних він обчислює відповідь без використання рекурсії.
  • Для рекурсивних випадків рекурсивних даних він обчислює відповідь за допомогою рекурсії над деструктурованими даними.

4

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

Щоб познайомити вас з реалізацією функції: Розгляньте наступний код віртуальної машини:

введіть тут опис зображення

Якщо Swift компілюється на цю мову віртуальної машини, то наступний блок коду Swift:

mult(a: 2, b: 3) - 4

складеться до

push constant 2  // Line 1
push constant 3  // Line 2
call mult        // Line 3
push constant 4  // Line 4
sub              // Line 5

Мова віртуальної машини розроблена навколо глобального стека . push constant nвисуває ціле число на цей глобальний стек.

Після виконання рядків 1 і 2 стек виглядає так:

256:  2  // Argument 0
257:  3  // Argument 1

256і 257є адресами пам'яті.

call mult висуває номер повернення (3) на стек і виділяє простір для локальних змінних функції.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  0  // local 0

... і це переходить до етикетки function mult. Код всередині multвиконується. В результаті виконання цього коду ми обчислюємо добуток 2 і 3, який зберігається у 0-й локальній змінній функції.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0

Перед початком роботи returnз мульти, ви помітите рядок:

push local 0  // push result

Ми висунемо виріб на стопку.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0
260:  6  // product

Коли ми повертаємось, трапляється таке:

  • Перемістіть останнє значення на стеку на адресу пам'яті 0-го аргументу (256 у цьому випадку). Це, мабуть, є найбільш зручним місцем для його розміщення.
  • Відкиньте все на стеку аж до адреси 0-го аргументу.
  • Перейдіть на номер зворотного рядка (3 у цьому випадку), а потім вперед.

Після повернення ми готові виконати рядок 4, і наш стек виглядає так:

256:  6  // product that we just returned

Тепер натискаємо 4 на стек.

256:  6
257:  4

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

Зараз у нас є

256:  2  // 6 - 4 = 2

Тепер, коли ви знаєте, як працює виклик функції, зрозуміти, як працює рекурсія, порівняно просто. Ніякої магії , просто стек.

Я реалізував вашу sumIntsфункцію на цій мові віртуальної машини:

function sumInts 0     // `0` means it has no local variables.
  label IF
    push argument 0
    push argument 1
    lte              
    if-goto ELSE_CASE
    push constant 0
    return
  label ELSE_CASE
    push constant 2
    push argument 0
    push constant 1
    add
    push argument 1
    call sumInts       // Line 15
    add                // Line 16
    return             // Line 17
// End of function

Зараз я назву це:

push constant 2
push constant 5
call sumInts           // Line 21

Код виконується, і ми дістаємось до місця зупинки, куди lteповертається false. Ось як виглядає стек у цей момент:

// First invocation
256:  2   // argument 0
257:  5   // argument 1
258:  21  // return line number
259:  2   // augend
// Second
260:  3   // argument 0
261:  5   // argument 1
262:  15  // return line number
263:  3   // augend
// Third
264:  4   // argument 0
265:  5   // argument 1
266:  15  // return line number
267:  4   // augend
// Fourth
268:  5   // argument 0
269:  5   // argument 1
270:  15  // return line number
271:  5   // augend
// Fifth
272:  6   // argument 0
273:  5   // argument 1
274:  15  // return line number
275:  0   // return value

Тепер давайте «розмотуємо» нашу рекурсію. return0 і перейти до лінії 15 і вперед.

271:  5
272:  0

Рядок 16: add

271:  5

Рядок 17: return5 і перехід до рядка 15 і вперед.

267:  4
268:  5

Рядок 16: add

267:  9

Рядок 17: return9 і перехід до рядка 15 і вперед.

263:  3
264:  9

Рядок 16: add

263:  12

Рядок 17: return12 і перехід до рядка 15 і вперед.

259:  2
260:  12

Рядок 16: add

259:  14

Рядок 17: return14 і перехід до лінії 21 і вперед.

256:  14

Там у вас є. Рекурсія: прославлена goto.


4

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

Я слідував за http://www.htdp.org/, який, окрім того, що є підручником із схеми, також є чудовим введенням у розробку програм з точки зору архітектури та дизайну.

Але в основному потрібно вкласти трохи часу. Без «твердого» розуміння рекурсії деякі алгоритми, такі як зворотний трек, завжди будуть здаватися вам «важкими» або навіть «магічними». Отже, наполегливо. :-D

Я сподіваюся, що це допомагає і удачі!


3

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

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


3

Трохи поза темою, я знаю, але ... спробуйте пошукати рекурсію в Google ... На прикладі ви побачите, що це означає :-)


Раніші версії Google повертали наступний текст (цитується з пам'яті):

Рекурсія

Див. Рекурсія

10 вересня 2014 року жарт про рекурсію оновлено:

Рекурсія

Ви мали на увазі: Рекурсія


Для іншої відповіді дивіться цю відповідь .


3

Подумайте про рекурсію, як кілька клонів роблять те саме ...

Ви просите клонувати [1]: "цифри суми між 2 і 5"

+ clone[1]               knows that: result is 2 + "sum numbers between 3 and 5". so he asks to clone[2] to return: "sum numbers between 3 and 5"
|   + clone[2]           knows that: result is 3 + "sum numbers between 4 and 5". so he asks to clone[3] to return: "sum numbers between 4 and 5"
|   |   + clone[3]       knows that: result is 4 + "sum numbers between 5 and 5". so he asks to clone[4] to return: "sum numbers between 5 and 5"
|   |   |   + clone[4]   knows that: result is 5 + "sum numbers between 6 and 5". so he asks to clone[5] to return: "sum numbers between 6 and 5"
|   |   |   |   clone[5] knows that: he can't sum, because 6 is larger than 5. so he returns 0 as result.
|   |   |   + clone[4]   gets the result from clone[5] (=0)  and sums: 5 + 0,  returning 5
|   |   + clone[3]       gets the result from clone[4] (=5)  and sums: 4 + 5,  returning 9
|   + clone[2]           gets the result from clone[3] (=9)  and sums: 3 + 9,  returning 12
+ clone[1]               gets the result from clone[2] (=12) and sums: 2 + 12, returning 14

і voilá !!


2

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

2, 3, 4, 5  //adding these numbers would sum to 14

Тепер зауважте, що ці рядки є заплутаними (не помилково, але заплутано).

if (a > b) {
    return 0 
}

Чому тест a>b?, і чомуreturn 0

Давайте змінимо код, щоб ближче відобразити те, що робить людина

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When 'a equals b' I'm at the most Right integer, return it
  }
  else {
    return a + sumInts(a: a + 1, b: b)
  }
}

Чи можемо ми зробити це ще по-людськи? Так! Зазвичай ми підсумовуємо зліва направо (2 + 3 + ...). Але наведена рекурсія підсумовується справа наліво (... + 4 + 5). Змініть код, щоб відобразити його (Це -може трохи залякати, але не дуже)

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When I'm at the most Left integer, return it
  }
  else {
    return sumInts(a: a, b: b - 1) + b
  }
}

Деякі можуть вважати цю функцію більш заплутаною, оскільки ми починаємо з "далекого" кінця, але на практиці можна почувати себе природним (і це ще одна хороша техніка "мислення": спробу "обох" сторін при вирішенні рекурсії). І знову, функція відображає те, що робить людина (більшість?): Бере суму всіх лівих цілих чисел і додає «наступне» право ціле число.


2

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

then now you will understand how recursion works now take a look of this post: Розуміння рекурсії крок за кроком

введіть тут опис зображення

Його програма:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

введіть тут опис зображення введіть тут опис зображення


2

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


0

Дозвольте розповісти вам на прикладі серії Фібоначчі

t (n) = t (n - 1) + n;

якщо n = 0, то 1

так що давайте подивимося , як рекурентні працює, я просто замінити nв t(n)с n-1і так далі. це виглядає:

t (n-1) = t (n - 2) + n + 1;

t (n-1) = t (n - 3) + n + 1 + n;

t (n-1) = t (n - 4) + n + 1 + n + 2 + n;

.

.

.

t (n) = t (nk) + ... + (nk-3) + (nk-2) + (nk-1) + n;

ми знаємо, що t(0)=(n-k)дорівнює 1тоді, n-k=0тому n=kзаміняємо kна n:

t (n) = t (nn) + ... + (n-n + 3) + (n-n + 2) + (n-n + 1) + n;

якщо пропустити n-nтоді:

t (n) = t (0) + ... + 3 + 2 + 1 + (n-1) + n;

так 3+2+1+(n-1)+nце натуральне число. він обчислюється якΣ3+2+1+(n-1)+n = n(n+1)/2 => n²+n/2

результат для fib: O(1 + n²) = O(n²)

Це найкращий спосіб зрозуміти рекурсивне відношення

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