Понимание того, как работают рекурсивные функции

Как поясняется в названии, у меня есть очень фундаментальный вопрос программирования, который я еще не смог проверить. Фильтрация всех (чрезвычайно умных) "Чтобы понять рекурсию, вы должны сначала понять рекурсию". ответы из разных онлайн-потоков я до сих пор не совсем понял.

Понимая, что, столкнувшись с незнанием того, чего мы не знаем, мы склонны задавать неправильные вопросы или задавать правильные вопросы неправильно. Я разделю то, что я "думаю", мой вопрос в надежде, что кто-то с похожим взглядом может поделиться некоторыми знаниями, которые помогут включить рекурсивную лампочку для меня!

Вот функция (синтаксис написан в 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 зависания:

  • Функция вызывается рекурсивно до тех пор, пока не будет выполнено условие. Это условие → b. Когда это условие выполнено, верните 0. На первый взгляд я ожидаю, что возвращаемое значение будет 0, что явно неверно.

  • Распечатка значения 'a' на каждой итерации дает значение, которое я ожидаю: 2, 3, 4, 5 (в какой точке 5 + 1 > b, который отвечает первому условию: a > b), но я до сих пор не вижу, как достигается значение 14.

Моя первая мысль заключается в том, что что-то похожее на следующее происходит магически:

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

Итак, изгоняя магию, я просто ничего не получаю. Я хотел бы понять, что происходит больше, чем просто неявно.

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

Ответ 1

Я думаю, что путаница возникает из-за того, что она думает об этом как о "той же функции", которая называется много раз. Если вы считаете, что это "много копий одной и той же функции", то она может быть более ясной:

Только одна копия функции когда-либо возвращает 0, и она не первая (она последняя). Таким образом, результат вызова первого не равен 0.

Для второго путаницы я думаю, что будет проще описать рекурсию на английском языке. Прочтите эту строку:

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

as "вернуть значение 'a' plus (возвращаемое значение другой копии функции, которая является значением копии 'a' plus (возвращаемое значение другой копии функции, которая является второй копией значение" a "плюс (...", причем каждая копия функции порождает новую копию самого себя с увеличением на 1, пока не будет выполнено условие a > b.

К тому времени, когда вы достигли условия a > b, являющегося истинным, у вас есть (потенциально произвольно) длинный стек копий функции, находящийся в середине выполнения, все ожидающие результата следующей копии, чтобы найти что они должны добавить к "а".

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

Ответ 2

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 мрамор для подсчета, вы просто возвращаете его обратно и говорите: 1. Пусть другие люди над вами делают добавление, которое вы сделали.

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

Позвольте мне показать вам шаги:

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 

Ответ 4

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

Код, указанный здесь, решает следующую проблему: вы хотите знать сумму всех целых чисел от 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, включительно, вы можете сделать следующее. Сначала вычислите сумму целых чисел между a + 1 и b включительно. Затем добавьте эту сумму. Вы заметите, что "вычислять сумму целых чисел между a + 1 и b, включительно", оказывается в значительной степени той же проблемой, которую мы уже пытаемся решить, но с немного разными параметрами. Вместо вычисления от a до b, включительно, мы вычисляем от + 1 до b включительно. То, что рекурсивный шаг - решить большую проблему ( "сумма от a до b, включительно" ), мы сводим проблему к меньшей версии самого себя ( "сумма от a + 1 до b, включительно" ).

Если вы посмотрите на код, который у вас выше, вы заметите, что в нем есть этот шаг:

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

Этот код - просто перевод приведенной выше логики - если вы хотите суммировать сумму от a до b включительно, начните с суммирования от + 1 до b включительно (что рекурсивный вызов sumInt s), затем добавьте a.

Конечно, сам по себе такой подход не будет работать. Например, как бы вы вычислили сумму всех целых чисел от 5 до 5 включительно? Ну, используя нашу текущую логику, вы вычислили сумму всех целых чисел от 6 до 5 включительно, затем добавьте 5. Итак, как вы вычисляете сумму всех целых чисел от 6 до 5 включительно? Ну, используя нашу текущую логику, вы вычислили сумму всех целых чисел от 7 до 5 включительно, а затем добавьте 6. Вы заметите проблему здесь - это просто продолжается и продолжается!

В рекурсивном решении проблемы должен быть какой-то способ остановить упрощение проблемы, а вместо этого просто решить ее напрямую. Как правило, вы найдете простой случай, когда ответ может быть определен немедленно, а затем структурируйте свое решение для решения простых случаев напрямую, когда они возникают. Обычно это называется базовым или рекурсивным.

Итак, какой базовый случай в этой конкретной проблеме? Когда вы суммируете целые числа от a до b, включительно, если a окажется больше b, тогда ответ равен 0 - в диапазоне нет чисел! Поэтому мы построим наше решение следующим образом:

  • Если a > b, тогда ответ равен 0.
  • В противном случае (a & le; b) получите ответ следующим образом:
    • Вычислить сумму целых чисел между a + 1 и b.
    • Добавьте ответ, чтобы получить ответ.

Теперь сравните этот псевдокод с вашим фактическим кодом:

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

Обратите внимание, что между решением, изложенным в псевдокоде, и этим фактическим кодом существует почти точно одно-одно. Первый шаг - базовый случай - в случае, если вы запрашиваете сумму пустого диапазона чисел, вы получаете 0. В противном случае вычислите сумму между a + 1 и b, а затем добавьте a.

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

Попробуй очень, очень простой случай. Что произойдет, если вы вызываете sumInts(6, 5)? В этом случае, прослеживая код, вы видите, что функция просто возвращает 0. Это правильная вещь, чтобы - в - нет никаких чисел в диапазоне. Теперь попробуй что-нибудь сложнее. Что происходит, когда вы вызываете sumInts(5, 5)? Ну, вот что происходит:

  • Вы вызываете sumInts(5, 5). Мы попадаем в ветвь else, которая возвращает значение `a + sumInts (6, 5).
  • Чтобы sumInts(5, 5) определить, что sumInts(6, 5), нам нужно приостановить то, что мы делаем, и сделать вызов sumInts(6, 5).
  • sumInts(6, 5) вызывается. Он входит в ветвь if и возвращает 0. Однако этот экземпляр sumInts был вызван sumInts(5, 5), поэтому возвращаемое значение возвращается к sumInts(5, 5), а не к вызывающему абоненту верхнего уровня.
  • 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 for sumInts (6, 5) , namely 0. It then returns 5 + 0 = 5`.
  • sumInts(4, 5) теперь имеет значение для sumInts(5, 5), а именно 5. Затем он возвращает 4 + 5 = 9.

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

Надеюсь, это поможет!

Ответ 5

Пока у вас есть несколько хороших ответов, но я добавлю еще один, который требует другого подхода.

Прежде всего, я написал много статей о простых рекурсивных алгоритмах, которые могут вас заинтересовать; увидеть

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 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 для замены, и мы могли бы тогда просто применить правила для арифметики.

Ответ 6

То, как я обычно выясняю, как работает рекурсивная функция, - это посмотреть на базовый регистр и работать в обратном направлении. Здесь этот метод применяется к этой функции.

Сначала базовый случай:

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)

Обратите внимание, что мы пришли к выводу о том, как работает функция, отслеживая вызовы в том порядке, в котором они возвращаются.

Ответ 7

Я отдам его.

Выполняя уравнение 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);
    }
}

И этого должно быть достаточно, чтобы подавить кого угодно, lol.;-P

Ответ 8

рекурсии. Рекурсия в области компьютерных наук подробно освещена в разделе "Конечные автоматы".

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

Это может быть иначе, если бы выражение было о том, что "мой автомобиль - это bentley, мой автомобиль синий". В этом случае замена во второй ситуации для автомобиля может быть "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 для ε заканчивает производство), и мы рекурсивно построили наше утверждение: "мой bentley синий".

Подумайте о рекурсии как о таких постановках и заменах. Каждый шаг процесса заменяет его предшественника, чтобы получить конечный результат. В точном примере рекурсивной суммы от 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

Ответ 9

Я думаю, что лучший способ понять рекурсивные функции - это понять, что они созданы для обработки рекурсивных структур данных. Но в вашей исходной функции sumInts(a: Int, b: Int), которая рекурсивно вычисляет сумму чисел от a до b, кажется, это не рекурсивная структура данных... Попробуйте слегка измененную версию sumInts(a: Int, n: Int), где n сколько номеров вы добавите.

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

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

Итак, 0 = Zero, 1 = Succesor (Zero), 2 = Succesor (Succesor (Zero)) и т.д.

Когда у вас есть рекурсивная структура данных, у вас есть шаблон для функции. Для каждого нерекурсивного случая вы можете вычислить значение напрямую. Для рекурсивных случаев вы предполагаете, что рекурсивная функция уже работает и использует ее для вычисления случая, но деконструирует аргумент. В случае 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)
    }
}

Приятно, что теперь вам не нужно думать о низком уровне рекурсии. Вам просто нужно проверить, что:

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

Ответ 10

Возможно, вас заинтересует Nisan и Schocken реализация функций. Связанный pdf файл является частью бесплатного онлайн-курса. В нем описывается вторая часть реализации виртуальной машины, в которой учащийся должен написать компилятор на языке виртуальной машины на языке машинного языка. Реализация функции, которую они предлагают, способна к рекурсии, поскольку она основана на стеках.

Представить вам реализацию функции: Рассмотрим следующий код виртуальной машины:

enter image description here

Если 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 ing из mult вы заметите строку:

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

Теперь давайте "размотаем" нашу рекурсию. return 0 и перейти к строке 15 и перейти вперед.

271:  5
272:  0

Строка 16: add

271:  5

Строка 17: return 5 и перейти к строке 15 и перейти вперед.

267:  4
268:  5

Строка 16: add

267:  9

Строка 17: return 9 и перейти к строке 15 и перейти вперед.

263:  3
264:  9

Строка 16: add

263:  12

Строка 17: return 12 и перейти к строке 15 и перейти вперед.

259:  2
260:  12

Строка 16: add

259:  14

Строка 17: return 14 и перейти к строке 21 и перейти вперед.

256:  14

Там у вас есть. Рекурсия: прославленная goto.

Ответ 11

Один очень хороший совет, с которым я столкнулся в обучении и действительно понимающий рекурсию, - это потратить некоторое время на изучение языка, который не имеет никакой конструкции цикла, кроме как через рекурсию. Таким образом, вы получите отличное представление о том, как использовать рекурсию через практику.

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

Но в принципе, вам нужно потратить некоторое время. Без "твердой" схватки рекурсии некоторые алгоритмы, такие как обратное отслеживание, всегда будут казаться "трудными" или даже "волшебными" для вас. Итак, упорствуй.:-D

Я надеюсь, что это поможет и удачи!

Ответ 12

Уже есть много хороших ответов. Тем не менее я даю попытку.
При вызове функция получает выделенное пространство памяти, которое укладывается в пространство памяти вызывающей функции. В этом пространстве памяти функция сохраняет переданные ему параметры, переменные и их значения. Это пространство памяти исчезает вместе с окончательным обратным вызовом функции. По мере того как идея стека идет, пространство памяти вызывающей функции теперь становится активным.

Для рекурсивных вызовов одна и та же функция получает несколько пространств памяти, сложенных друг на друга. Все это. Простое представление о том, как стек работает в памяти компьютера, должно помочь вам понять, как рекурсия происходит в реализации.

Ответ 13

Немного не по теме, я знаю, но... попробуйте найти рекурсию в Google... Вы увидите на примере, что это значит: -)


Более ранние версии Google вернули следующий текст (процитированный из памяти):

рекурсии

См. Рекурсия

10 сентября 2014 года была обновлена ​​шутка о рекурсии:

рекурсии

Возможно, вы имели в виду: Recursion


Для получения другого ответа см. этот ответ.

Ответ 14

Думайте рекурсию как несколько клонов, делая то же самое...

Вы просите клонировать [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á!!

Ответ 15

Многие из приведенных выше ответов очень хороши. Однако полезный метод решения рекурсии состоит в том, чтобы сначала изложить то, что мы хотим сделать, и кодировать, поскольку человек разрешил бы это. В приведенном выше случае мы хотим суммировать последовательность последовательных целых чисел (используя числа сверху):

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
  }
}

Некоторые могут найти эту функцию более запутанной, так как мы начинаем с "дальнего" конца, но практика может заставить ее чувствовать себя естественной (и это еще одна хорошая методика "мышления": попытка "обеих сторон" при решении рекурсии). И снова функция отражает то, что делает человек (большинство?): Принимает сумму всех левых целых чисел и добавляет "следующее" целое число.

Ответ 16

Мне было трудно понять рекурсию, после чего я нашел этот блог, и я уже видел этот вопрос, поэтому я подумал, что должен поделиться. Вы должны прочитать этот блог, я обнаружил, что это очень полезно объяснить его стеком, и даже объясняет, как две рекурсии работают со стеком шаг за шагом. Я рекомендую вам сначала понять, как работает стек, который он объясняет здесь очень хорошо: путешествие в стек

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)

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

Ответ 17

Рекурсия начала иметь смысл для меня, когда я перестала читать то, что другие говорят об этом, или рассматривая ее как нечто, что я могу избежать, и просто написал код. Я нашел проблему с решением и попытался дублировать решение, не глядя. Я только посмотрел на решение, когда я беспомощно застрял. Затем я вернулся, пытаясь его дублировать. Я сделал это снова по нескольким проблемам, пока не разработал свое собственное понимание и понимание того, как определить рекурсивную проблему и решить ее. Когда я дошел до этого уровня, я начал решать проблемы и решать их. Это помогло мне больше. Иногда, вещи можно узнать только, пытаясь самостоятельно и бороться; пока вы не "получите".

Ответ 18

Позвольте мне рассказать вам на примере ряда Фибоначчи, Фибоначчи

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)=(nk) равно 1 то nk=0 поэтому n=k заменим k на n:

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

если мы опускаем nn то:

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²)

Это лучший способ понять рекурсивное отношение