Для любопытных: и в то время, и в то время как были на языке в течение очень долгого времени. Пока использовался в старом английском; в то время как среднеанглийское развитие while. Как союзы они взаимозаменяемы по значению, но пока не сохранились в стандартном американском английском.
Филипп Бартузи
14
Может быть, уже поздно, но это довольно хорошая статья о хвостовой
рекурсии
5
Одним из значительных преимуществ идентификации хвостовой рекурсивной функции является то, что она может быть преобразована в итеративную форму и, таким образом, освободить алгоритм от накладных расходов на метод. Возможно, вам понравятся отклики @Kyle Cronin и нескольких других, представленных ниже
KGhatak
Эта ссылка от @yesudeep - лучшее, самое подробное описание, которое я нашел - lua.org/pil/6.3.html
Джефф Фишер
1
Может ли кто-нибудь сказать мне: сортировка слиянием и быстрая сортировка используют хвостовую рекурсию (TRO)?
majurageerthan
Ответы:
1722
Рассмотрим простую функцию, которая добавляет первые N натуральных чисел. (например sum(5) = 1 + 2 + 3 + 4 + 5 = 15).
Вот простая реализация JavaScript, которая использует рекурсию:
function recsum(x){if(x ===1){return x;}else{return x + recsum(x -1);}}
Если бы вы позвонили recsum(5), это то, что интерпретатор JavaScript будет оценивать:
Вот последовательность событий, которые произошли бы, если бы вы вызвали tailrecsum(5)(что будет эффективно tailrecsum(5, 0)из-за второго аргумента по умолчанию).
В случае хвостовой рекурсии, с каждой оценкой рекурсивного вызова, running_totalобновляется.
Примечание: в оригинальном ответе использованы примеры из Python. Они были изменены на JavaScript, поскольку интерпретаторы Python не поддерживают оптимизацию хвостового вызова . Однако, хотя оптимизация хвостовых вызовов является частью спецификации ECMAScript 2015 , большинство интерпретаторов JavaScript не поддерживают ее .
Могу ли я сказать, что с хвостовой рекурсией окончательный ответ рассчитывается по последнему вызову одного метода? Если это НЕ хвостовая рекурсия, вам нужны все результаты для всех методов, чтобы вычислить ответ.
chrisapotek
2
Вот приложение, которое представляет несколько примеров на Lua: lua.org/pil/6.3.html Может быть полезно пройти через это! :)
yesudeep
2
Может кто-нибудь ответить на вопрос chrisapotek? Я запутался, как tail recursionэтого достичь на языке, который не оптимизирует удаленные вызовы.
Кевин Мередит
3
@KevinMeredith «хвостовая рекурсия» означает, что последний оператор в функции - это рекурсивный вызов той же функции. Вы правы, что нет смысла делать это на языке, который не оптимизирует эту рекурсию. Тем не менее, этот ответ действительно показывает концепцию (почти) правильно. Это было бы более ясно, если бы "else:" было опущено. Не изменил бы поведение, но поместил бы хвостовой вызов как независимое утверждение. Я представлю это как редактирование.
ToolmakerSteve
2
Таким образом, в python нет никакого преимущества, потому что при каждом вызове функции tailrecsum создается новый кадр стека - верно?
Quazi Irfan
708
В традиционной рекурсии типичная модель состоит в том, что вы сначала выполняете свои рекурсивные вызовы, а затем берете возвращаемое значение рекурсивного вызова и вычисляете результат. Таким образом, вы не получите результат своих расчетов, пока не вернетесь после каждого рекурсивного вызова.
В хвостовой рекурсии вы сначала выполняете свои вычисления, а затем выполняете рекурсивный вызов, передавая результаты текущего шага следующему рекурсивному шагу. Это приводит к тому, что последнее утверждение находится в форме (return (recursive-function params)). По сути, возвращаемое значение любого заданного рекурсивного шага совпадает с возвращаемым значением следующего рекурсивного вызова .
Следствием этого является то, что, как только вы будете готовы выполнить следующий рекурсивный шаг, вам больше не нужен текущий кадр стека. Это учитывает некоторую оптимизацию. На самом деле, с соответствующим письменным компилятором, вы никогда не должны иметь переполнение стека смешок с хвостом рекурсивного вызова. Просто используйте текущий кадр стека для следующего рекурсивного шага. Я почти уверен, что Лисп делает это.
«Я почти уверен, что Лисп делает это», - делает Схема, но Common Lisp не всегда.
Аарон
2
@Daniel "По сути, возвращаемое значение любого заданного рекурсивного шага совпадает с возвращаемым значением следующего рекурсивного вызова." - Я не вижу этого аргумента, сохраняющего значение true для фрагмента кода, опубликованного Лорин Хохштайн. Можете ли вы уточнить?
Компьютерщик
8
@Geek Это действительно запоздалый ответ, но это действительно так в примере Лорин Хохштайн. Расчет для каждого шага выполняется до рекурсивного вызова, а не после него. В результате каждая остановка просто возвращает значение непосредственно из предыдущего шага. Последний рекурсивный вызов завершает вычисление, а затем возвращает окончательный результат без изменений полностью вниз по стеку вызовов.
Рейраб
3
Scala делает, но вам нужен @tailrec, указанный для его применения.
SilentDirge
2
«Таким образом, вы не получите результат своих расчетов, пока не вернетесь после каждого рекурсивного вызова». - может быть, я неправильно понял это, но это не особенно верно для ленивых языков, где традиционная рекурсия - единственный способ получить результат без вызова всех рекурсий (например, сворачивание бесконечного списка Bools с & &).
hasufell
206
Важным моментом является то, что хвостовая рекурсия по существу эквивалентна зацикливанию. Это не просто вопрос оптимизации компилятора, а фундаментальный факт выразительности. Это идет в обе стороны: вы можете взять любую петлю в форме
while(E){ S };return Q
где E и Qявляются выражениями и Sпредставляет собой последовательность операторов, и превращают ее в хвостовую рекурсивную функцию
f()=if E then { S;return f()}else{return Q }
Конечно, E , Sи Qдолжны быть определены , чтобы вычислить некоторые интересные значения по некоторым переменным. Например, функция зацикливания
sum(n){int i =1, k =0;while( i <= n ){
k += i;++i;}return k;}
эквивалентна хвостовой рекурсивной функции
sum_aux(n,i,k){if( i <= n ){return sum_aux(n,i+1,k+i);}else{return k;}}
sum(n){return sum_aux(n,1,0);}
(Это «обертывание» хвостовой рекурсивной функции с функцией с меньшим количеством параметров является обычной функциональной идиомой.)
В ответе @LorinHochstein я понял, основываясь на его объяснении, что хвостовая рекурсия должна быть, когда рекурсивная часть следует за «возвратом», однако у вас хвостовая рекурсия - нет. Вы уверены, что ваш пример правильно считается хвостовой рекурсией?
CodyBugstein
1
@Imray Хвосто-рекурсивная часть - это оператор return sum_aux внутри sum_aux.
Крис Конвей
1
@lmray: код Криса по сути эквивалентен. Порядок if / then и стиль ограничивающего теста ... if x == 0 против if (i <= n) ... - это не то, за что можно зацикливаться. Дело в том, что каждая итерация передает свой результат следующей.
Тейлор
else { return k; }можно изменить наreturn k;
c0der
144
Этот отрывок из книги « Программирование на Lua» показывает, как сделать правильную рекурсию хвоста (на Lua, но должна применяться и к Lisp) и почему это лучше.
Хвост вызова [хвост рекурсии] является своего рода Goto , одетый , как вызов. Хвостовой вызов происходит, когда функция вызывает другое как последнее действие, так что ей больше нечего делать. Например, в следующем коде вызов gявляется хвостовым вызовом:
function f (x)return g(x)end
После fзвонков gбольше делать нечего. В таких ситуациях программе не нужно возвращаться к вызывающей функции после завершения вызываемой функции. Поэтому после хвостового вызова программе не нужно хранить какую-либо информацию о вызывающей функции в стеке. ...
Поскольку правильный хвостовой вызов не использует пространство стека, нет ограничения на количество «вложенных» хвостовых вызовов, которые может выполнить программа. Например, мы можем вызвать следующую функцию с любым числом в качестве аргумента; он никогда не переполнит стек:
function foo (n)if n >0thenreturn foo(n -1)endend
... Как я уже говорил ранее, хвостовой вызов - это своего рода goto. Как таковое, весьма полезное применение правильных хвостовых вызовов в Lua для программирования конечных автоматов. Такие приложения могут представлять каждое состояние функцией; изменить состояние означает перейти (или вызвать) определенную функцию. В качестве примера рассмотрим простую игру-лабиринт. В лабиринте есть несколько комнат, каждая из которых имеет до четырех дверей: север, юг, восток и запад. На каждом шаге пользователь вводит направление движения. Если в этом направлении есть дверь, пользователь идет в соответствующую комнату; в противном случае программа выводит предупреждение. Цель состоит в том, чтобы перейти из начальной комнаты в последнюю комнату.
Эта игра представляет собой типичный конечный автомат, где текущая комната - это состояние. Мы можем реализовать такой лабиринт с одной функцией для каждой комнаты. Мы используем хвостовые вызовы для перемещения из одной комнаты в другую. Небольшой лабиринт с четырьмя комнатами может выглядеть так:
Итак, вы видите, когда вы делаете рекурсивный вызов вроде:
function x(n)if n==0thenreturn0
n= n-2return x(n)+1end
Это не хвостовая рекурсия, потому что у вас еще есть чем заняться (добавить 1) в этой функции после выполнения рекурсивного вызова. Если вы введете очень большое число, это, вероятно, приведет к переполнению стека.
Это отличный ответ, потому что он объясняет влияние хвостовых вызовов на размер стека.
Эндрю Свон
@AndrewSwan Действительно, хотя я полагаю, что первоначальному спрашивающему и случайному читателю, который может наткнуться на этот вопрос, может быть лучше подан с принятым ответом (поскольку он может не знать, что такое стек на самом деле.) Кстати, я использую Jira, большой поклонник.
Гофман
1
Мой любимый ответ также из-за включения значения размера стека.
njk2015
80
Используя обычную рекурсию, каждый рекурсивный вызов помещает другую запись в стек вызовов. Когда рекурсия завершена, приложение должно вытолкнуть каждую запись обратно вниз.
При использовании хвостовой рекурсии, в зависимости от языка, компилятор может свернуть стек до одной записи, поэтому вы экономите место в стеке ... Большой рекурсивный запрос может фактически вызвать переполнение стека.
В основном рекурсии Tail могут быть оптимизированы в итерацию.
«Большой рекурсивный запрос может вызвать переполнение стека». должен быть в первом абзаце, а не во втором (хвостовой рекурсии)? Большим преимуществом хвостовой рекурсии является то, что она может быть (например, схема) оптимизирована таким образом, чтобы не «накапливать» вызовы в стеке, поэтому в большинстве случаев она позволит избежать переполнения стека!
Оливье Дюлак
70
В файле жаргона есть это, чтобы сказать об определении хвостовой рекурсии:
хвостовая рекурсия / n ./
Если вы уже не устали от этого, посмотрите хвостовую рекурсию.
Вместо объяснения словами, вот пример. Это версия Scheme факториальной функции:
(define(factorial x)(if(= x 0)1(* x (factorial (- x 1)))))
Вот версия факториала с хвостовой рекурсией:
(define factorial
(letrec ((fact (lambda(x accum)(if(= x 0) accum
(fact (- x 1)(* accum x))))))(lambda(x)(fact x 1))))
В первой версии вы заметите, что рекурсивный вызов факта подается в выражение умножения, и поэтому при выполнении рекурсивного вызова состояние должно быть сохранено в стеке. В хвостовой рекурсивной версии нет другого S-выражения, ожидающего значение рекурсивного вызова, и, поскольку дальнейших действий не требуется, состояние не нужно сохранять в стеке. Как правило, хвостовые рекурсивные функции Scheme используют постоянное пространство стека.
+1 за упоминание самого важного аспекта хвостовых рекурсий, заключающегося в том, что они могут быть преобразованы в итеративную форму и, таким образом, превращены в форму сложности памяти O (1).
К.Гатак
1
@KGhatak не совсем; Ответ правильно говорит о «постоянном пространстве стека», а не о памяти вообще. не придираться, просто чтобы убедиться, что нет недопонимания. например, хвост-рекурсивный список-хвост-мутированиеlist-reverse будет выполняться в пространстве постоянного стека, но создаст и увеличит структуру данных в куче. Обход дерева может использовать моделируемый стек в качестве дополнительного аргумента. и т. д.
Уилл Несс
45
Хвостовая рекурсия относится к рекурсивному вызову, который является последним в последней логической инструкции в рекурсивном алгоритме.
Обычно в рекурсии у вас есть базовый случай, который останавливает рекурсивные вызовы и начинает выталкивать стек вызовов. Чтобы использовать классический пример, хотя и больше C-ish, чем Lisp, функция факториала иллюстрирует хвостовую рекурсию. Рекурсивный вызов происходит после проверки условия базового случая.
Мне показалось, что ваше объяснение проще всего понять, но если это что-то и нужно, то хвостовая рекурсия полезна только для функций с одним оператором в базовых случаях. Рассмотрим такой метод, как этот postimg.cc/5Yg3Cdjn . Примечание: внешний else- это шаг, который вы можете назвать «базовым случаем», но он охватывает несколько строк. Я вас неправильно понимаю или мое предположение верно? Хвостовая рекурсия хороша только для одного лайнера?
Я хочу ответы
2
@IWantAnswers - Нет, тело функции может быть сколь угодно большим. Все, что требуется для хвостового вызова, - это то, что ветка, в которой он находится, вызывает функцию как самое последнее, что она делает, и возвращает результат вызова функции. factorialПример просто классический простой пример, это все.
TJ Crowder
28
Это означает, что вместо необходимости помещать указатель инструкций в стек, вы можете просто перейти к вершине рекурсивной функции и продолжить выполнение. Это позволяет функциям возвращаться бесконечно, не переполняя стек.
Я написал пост в блоге на эту тему, в котором есть графические примеры того, как выглядят фреймы стека.
Вот быстрый фрагмент кода, сравнивающий две функции. Первый - это традиционная рекурсия для поиска факториала заданного числа. Второй использует хвостовую рекурсию.
Очень просто и интуитивно понятно.
Простой способ определить, является ли рекурсивная функция хвостовой рекурсивной, - это если она возвращает конкретное значение в базовом случае. Это означает, что он не возвращает 1 или true или что-то подобное. Скорее всего, он вернет какой-то вариант одного из параметров метода.
Другой способ - определить, свободен ли рекурсивный вызов от каких-либо сложений, арифметических действий, модификаций и т. Д. Это означает, что это не что иное, как чистый рекурсивный вызов.
0! 1. Так что «mynumber == 1» должно быть «mynumber == 0».
Полерто
19
Лучший способ для меня понять tail call recursion- это особый случай рекурсии, когда последний вызов (или хвостовой вызов) - это сама функция.
Сравнение примеров, представленных в Python:
def recsum(x):
if x == 1:
return x
else:
return x + recsum(x - 1)
^ RECURSION
def tailrecsum(x, running_total=0):
if x == 0:
return running_total
else:
return tailrecsum(x - 1, running_total + x)
^ ХВОСТ РЕКУРСИЯ
Как вы можете видеть , в общем рекурсивной версии, окончательный вызов в блоке кода является x + recsum(x - 1). Таким образом, после вызова recsumметода есть еще одна операция x + ...
Однако в хвостовой рекурсивной версии последний вызов (или хвостовой вызов) в блоке кода tailrecsum(x - 1, running_total + x) означает, что последний вызов сделан самому методу, и после этого никакой операции не происходит.
Этот момент важен, потому что хвостовая рекурсия, как видно здесь, не заставляет память расти, потому что, когда базовая виртуальная машина видит функцию, вызывающую себя в хвостовой позиции (последнее выражение, которое должно быть оценено в функции), она устраняет текущий кадр стека, который известен как Оптимизация Tail Call (TCO).
РЕДАКТИРОВАТЬ
NB. Помните, что приведенный выше пример написан на Python, среда выполнения которого не поддерживает TCO. Это всего лишь пример, чтобы объяснить суть. TCO поддерживается на таких языках, как Scheme, Haskell и т. Д.
Вот пример Common Lisp, который выполняет факториалы с использованием хвостовой рекурсии. Из-за природы без стека, можно выполнять безумно большие факторные вычисления ...
Короче говоря, хвостовая рекурсия имеет рекурсивный вызов как последний оператор в функции, так что ей не нужно ждать рекурсивного вызова.
Так что это хвостовая рекурсия, т. Е. N (x - 1, p * x) - это последний оператор в функции, где компилятор умен, чтобы выяснить, что его можно оптимизировать для цикла for (factorial). Второй параметр p несет значение промежуточного продукта.
function N(x, p) {
return x == 1 ? p : N(x - 1, p * x);
}
Это нерекурсивный способ написания вышеупомянутой факториальной функции (хотя некоторые компиляторы C ++ в любом случае могут ее оптимизировать).
function N(x) {
return x == 1 ? 1 : x * N(x - 1);
}
но это не так
function F(x) {
if (x == 1) return 0;
if (x == 2) return 1;
return F(x - 1) + F(x - 2);
}
N (x-1) - это последнее утверждение в функции, где компилятор умен, чтобы понять, что его можно оптимизировать для цикла for (factorial)
doctorlai
Меня беспокоит то, что ваша функция N - это именно функция recsum из принятого ответа по этой теме (за исключением того, что это сумма, а не произведение), и что recsum называется нерекурсивным?
Фабиан Пийке
8
Вот версия Perl 5 tailrecsumфункции, упомянутой ранее.
sub tail_rec_sum($;$){my( $x,$running_total )=(@_,0);
return $running_total unless $x;@_=($x-1,$running_total+$x);goto&tail_rec_sum;# throw away current stack frame}
В отличие от итерации и рекурсии, мы должны быть осторожны, чтобы не перепутать понятие рекурсивного процесса с понятием рекурсивной процедуры. Когда мы описываем процедуру как рекурсивную, мы ссылаемся на синтаксический факт, что определение процедуры ссылается (прямо или косвенно) на саму процедуру. Но когда мы описываем процесс следующим образом, скажем, линейно рекурсивным, мы говорим о том, как этот процесс развивается, а не о синтаксисе написания процедуры. Может показаться тревожным, что мы ссылаемся на рекурсивную процедуру, такую как факторинг, как на создание итеративного процесса. Однако процесс действительно итеративный: его состояние полностью фиксируется тремя переменными состояния, и интерпретатору необходимо отслеживать только три переменные, чтобы выполнить процесс.
Одна из причин, по которой различие между процессом и процедурой может сбивать с толку, заключается в том, что большинство реализаций общих языков (включая Ada, Pascal и C) спроектированы таким образом, что интерпретация любой рекурсивной процедуры потребляет объем памяти, который увеличивается с ростом число вызовов процедур, даже если описанный процесс в принципе является итеративным. Как следствие, эти языки могут описывать итерационные процессы только путем обращения к специальным «циклическим конструкциям», таким как do, repeat, before, for и while. Реализация Схемы не разделяет этот недостаток. Он будет выполнять итеративный процесс в постоянном пространстве, даже если итерационный процесс описывается рекурсивной процедурой. Реализация с этим свойством называется хвостовой рекурсией.В хвостовой рекурсивной реализации итерация может быть выражена с использованием обычного механизма вызова процедуры, так что специальные конструкции итерации полезны только как синтаксический сахар.
Я прочитал все ответы здесь, и все же это самое ясное объяснение, которое касается действительно глубокого ядра этой концепции. Это объясняет это так прямо, что все выглядит так просто и понятно. Прости мою грубость, пожалуйста. Это как-то заставляет меня чувствовать, что другие ответы просто не бьют по голове. Я думаю, именно поэтому SICP имеет значение.
englealuze
8
Рекурсивная функция - это функция, которая вызывает сама
Это позволяет программистам писать эффективные программы, используя минимальный объем кода .
Недостатком является то, что они могут вызвать бесконечные циклы и другие неожиданные результаты, если они не записаны должным образом .
Я объясню и простую рекурсивную функцию, и хвостовую рекурсивную функцию
Для того, чтобы написать простую рекурсивную функцию
Первое, что нужно рассмотреть, это когда вы решите выйти из цикла, который является циклом if
Во-вторых, что делать, если мы являемся нашей собственной функцией
Из приведенного примера:
public static int fact(int n){
if(n <=1)
return 1;
else
return n * fact(n-1);
}
Из приведенного выше примера
if(n <=1)
return 1;
Является ли решающим фактором, когда выйти из цикла
else
return n * fact(n-1);
Фактическая обработка должна быть сделана
Позвольте мне решить задачу один за другим для облегчения понимания.
Давайте посмотрим, что произойдет внутри, если я бегу fact(4)
Подставляя n = 4
public static int fact(4){
if(4 <=1)
return 1;
else
return 4 * fact(4-1);
}
Ifцикл завершается неудачно, поэтому он переходит в elseцикл, поэтому он возвращает4 * fact(3)
В стековой памяти мы имеем 4 * fact(3)
Подставляя n = 3
public static int fact(3){
if(3 <=1)
return 1;
else
return 3 * fact(3-1);
}
Ifцикл завершается неудачно, поэтому он переходит в elseцикл
так что возвращается 3 * fact(2)
Помните, мы назвали `` `4 * fact (3)` `
Выход для fact(3) = 3 * fact(2)
Пока стек имеет 4 * fact(3) = 4 * 3 * fact(2)
В стековой памяти мы имеем 4 * 3 * fact(2)
Подставляя n = 2
public static int fact(2){
if(2 <=1)
return 1;
else
return 2 * fact(2-1);
}
Ifцикл завершается неудачно, поэтому он переходит в elseцикл
так что возвращается 2 * fact(1)
Помните, мы звонили 4 * 3 * fact(2)
Выход для fact(2) = 2 * fact(1)
Пока стек имеет 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
В стековой памяти мы имеем 4 * 3 * 2 * fact(1)
Подставляя n = 1
public static int fact(1){
if(1 <=1)
return 1;
else
return 1 * fact(1-1);
}
If петля верна
так что возвращается 1
Помните, мы звонили 4 * 3 * 2 * fact(1)
Выход для fact(1) = 1
Пока стек имеет 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Наконец, результат факта (4) = 4 * 3 * 2 * 1 = 24
Хвостовая рекурсия будет
public static int fact(x, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(x-1, running_total*x);
}
}
Подставляя n = 4
public static int fact(4, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(4-1, running_total*4);
}
}
Ifцикл завершается неудачно, поэтому он переходит в elseцикл, поэтому он возвращаетfact(3, 4)
В стековой памяти мы имеем fact(3, 4)
Подставляя n = 3
public static int fact(3, running_total=4) {
if (x==1) {
return running_total;
} else {
return fact(3-1, 4*3);
}
}
Ifцикл завершается неудачно, поэтому он переходит в elseцикл
так что возвращается fact(2, 12)
В стековой памяти мы имеем fact(2, 12)
Подставляя n = 2
public static int fact(2, running_total=12) {
if (x==1) {
return running_total;
} else {
return fact(2-1, 12*2);
}
}
Ifцикл завершается неудачно, поэтому он переходит в elseцикл
так что возвращается fact(1, 24)
В стековой памяти мы имеем fact(1, 24)
Подставляя n = 1
public static int fact(1, running_total=24) {
if (x==1) {
return running_total;
} else {
return fact(1-1, 24*1);
}
}
Хвостовая рекурсия - это жизнь, которой вы живете сейчас. Вы постоянно перерабатываете один и тот же кадр стека, снова и снова, потому что нет причин или средств для возврата к «предыдущему» кадру. С прошлым покончено, и с ним можно отказаться. Вы получаете один кадр, навсегда двигаясь в будущее, пока ваш процесс неизбежно не умрет.
Аналогия нарушается, если учесть, что некоторые процессы могут использовать дополнительные кадры, но все равно считаются хвостово-рекурсивными, если стек не растет бесконечно.
оно не нарушается при интерпретации расщепления личности . :) Общество разума; Разум как общество. :)
Уилл Несс
Вот Это Да! Теперь это еще один способ думать об этом
sutanu dalui
7
Хвостовая рекурсия - это рекурсивная функция, в которой функция вызывает себя в конце («хвосте») функции, в которой после возврата рекурсивного вызова вычисления не выполняются. Многие компиляторы оптимизируют замену рекурсивного вызова на хвостовой рекурсивный или итеративный вызов.
Рассмотрим проблему вычисления факториала числа.
Прямой подход будет:
factorial(n):
if n==0 then 1
else n*factorial(n-1)
Предположим, вы звоните факториалом (4). Дерево рекурсии будет:
Здесь также максимальная глубина рекурсии равна O (n), но ни один из вызовов не добавляет никакой дополнительной переменной в стек. Следовательно, компилятор может покончить со стеком.
Хвостовая рекурсия довольно быстрая по сравнению с обычной рекурсией. Это быстро, потому что вывод вызова предков не будет записан в стек, чтобы сохранить трек. Но в обычной рекурсии все предки вызывают вывод, записанный в стеке, чтобы сохранить трек.
Хвост рекурсивная функция является рекурсивной функцией , где последняя операция это делает , прежде чем Возвращаться сделать вызов рекурсивной функции. То есть возвращаемое значение рекурсивного вызова функции сразу возвращается. Например, ваш код будет выглядеть так:
def recursiveFunction(some_params):
# some code here
return recursiveFunction(some_args)
# no code after the return statement
Компиляторы и интерпретаторы, которые реализуют оптимизацию хвостовых вызовов или устранение хвостовых вызовов, могут оптимизировать рекурсивный код для предотвращения переполнения стека. Если ваш компилятор или интерпретатор не реализует оптимизацию хвостовых вызовов (например, интерпретатор CPython), то нет никакой дополнительной выгоды для написания вашего кода таким способом.
Например, это стандартная рекурсивная факториальная функция в Python:
def factorial(number):
if number == 1:
# BASE CASE
return 1
else:
# RECURSIVE CASE
# Note that `number *` happens *after* the recursive call.
# This means that this is *not* tail call recursion.
return number * factorial(number - 1)
И это рекурсивная версия хвостового вызова факториальной функции:
def factorial(number, accumulator=1):
if number == 0:
# BASE CASE
return accumulator
else:
# RECURSIVE CASE
# There's no code after the recursive call.
# This is tail call recursion:
return factorial(number - 1, number * accumulator)
print(factorial(5))
(Обратите внимание, что, хотя это и есть код Python, интерпретатор CPython не выполняет оптимизацию хвостовых вызовов, поэтому такая организация вашего кода не дает никаких преимуществ во время выполнения.)
Возможно, вам придется сделать свой код немного более нечитаемым, чтобы использовать оптимизацию хвостовых вызовов, как показано в примере факториала. (Например, базовый случай теперь немного неинтуитивен, иaccumulator параметр эффективно используется как своего рода глобальная переменная.)
Но преимущество оптимизации хвостового вызова состоит в том, что она предотвращает ошибки переполнения стека. (Я отмечу, что вы можете получить то же преимущество, используя итеративный алгоритм вместо рекурсивного.)
Переполнения стека возникают, когда в стеке вызовов слишком много объектов кадра. Объект кадра помещается в стек вызовов при вызове функции и выталкивается из стека вызовов при возврате функции. Объекты Frame содержат информацию, такую как локальные переменные и строку кода, к которой нужно вернуться, когда функция вернется.
Если ваша рекурсивная функция делает слишком много рекурсивных вызовов без возврата, стек вызовов может превысить свой предел объекта фрейма. (Число зависит от платформы; в Python по умолчанию это 1000 кадровых объектов.) Это вызывает переполнение стека ошибку . (Эй, отсюда и название этого сайта!)
Однако, если последнее, что делает ваша рекурсивная функция, это делает рекурсивный вызов и возвращает его возвращаемое значение, то нет никаких причин для того, чтобы поддерживать текущий объект фрейма в стеке вызовов. В конце концов, если после рекурсивного вызова функции нет кода, нет причин зависать от локальных переменных текущего фрейма. Таким образом, мы можем немедленно избавиться от текущего объекта фрейма, а не сохранять его в стеке вызовов. Конечным результатом этого является то, что ваш стек вызовов не увеличивается в размере и, следовательно, не может переполняться стеком.
Компилятор или интерпретатор должен иметь функцию хвостового вызова в качестве функции, чтобы он мог распознавать, когда может быть применена оптимизация хвостового вызова. Даже в этом случае вы могли бы переупорядочить код в своей рекурсивной функции, чтобы использовать оптимизацию хвостовых вызовов, и вам решать, стоит ли это потенциальное снижение читабельности оптимизации.
Msgstr "Хвостовая рекурсия (также называемая оптимизацией хвостового вызова или устранением хвостового вызова)". Нет; Исключение хвостовых вызовов или оптимизация хвостовых вызовов - это то, что вы можете применить к хвостовой рекурсивной функции, но это не одно и то же. Вы можете написать хвостовые рекурсивные функции в Python (как вы показываете), но они не более эффективны, чем нерекурсивные функции, потому что Python не выполняет оптимизацию хвостовых вызовов.
chepner
Значит ли это, что если кому-то удастся оптимизировать веб-сайт и сделать рекурсивный вызов хвостовым рекурсивным, у нас больше не будет сайта StackOverflow ?! Это ужасно.
Наджиб Мами
5
Чтобы понять некоторые основные различия между рекурсией хвостового вызова и рекурсией не хвостового вызова, мы можем изучить реализации этих технологий в .NET.
Если вам нужно знать, какие условия мешают компилятору C # выполнять оптимизацию хвостового вызова, см. Эту статью: Условия хвостового вызова JIT CLR .
Существует два основных вида рекурсии: рекурсия головы и хвостовая рекурсия.
В рекурсии головы функция выполняет свой рекурсивный вызов, а затем выполняет еще несколько вычислений, возможно, используя, например, результат рекурсивного вызова.
В хвостовой рекурсивной функции все вычисления выполняются первыми, а рекурсивный вызов - последним.
Взято из этого супер классного поста. Пожалуйста, подумайте над прочтением.
В вспомогательной процедуре последнее, что она делает, если left не равно nil, - это вызывает себя (ПОСЛЕ того, что что-то минует и что-то cdr). Это в основном, как вы отображаете список.
Хвостовая рекурсия имеет большое преимущество в том, что интерпретатор (или компилятор, зависящий от языка и поставщика) может оптимизировать его и преобразовать в нечто, эквивалентное циклу while. На самом деле, в традиции Scheme, большинство циклов for и while выполняется методом хвостовой рекурсии (насколько я знаю, нет и for, и while).
На этот вопрос есть много хороших ответов ... но я не могу не согласиться с альтернативным подходом к определению «рекурсии хвоста» или, по крайней мере, «правильной рекурсии хвоста». А именно: следует ли рассматривать его как свойство определенного выражения в программе? Или следует рассматривать это как свойство реализации языка программирования ?
Более подробно о последнем представлении есть классическая статья Уилла Клингера «Правильная рекурсия хвоста и эффективность использования пространства» (PLDI 1998), в которой «правильная хвостовая рекурсия» определяется как свойство реализации языка программирования. Определение построено так, чтобы позволить игнорировать подробности реализации (например, представлен ли стек вызовов на самом деле через стек времени выполнения или через выделенный в куче связанный список кадров).
Для этого используется асимптотический анализ: не времени выполнения программы, как обычно, а скорее использования пространства программы . Таким образом, использование пространства распределенного списка, выделенного в куче, по сравнению со стеком вызовов времени выполнения оказывается асимптотически эквивалентным; таким образом, можно игнорировать эту деталь реализации языка программирования (деталь, которая, безусловно, имеет большое значение на практике, но может немного испачкать воду, когда кто-то пытается определить, удовлетворяет ли данная реализация требованию «рекурсивности хвоста свойства»). )
Статья заслуживает тщательного изучения по ряду причин:
Он дает индуктивное определение хвостовых выражений и хвостовых вызовов программы. (Такое определение и почему такие звонки важны, по-видимому, является предметом большинства других ответов, приведенных здесь.)
Вот эти определения, просто чтобы дать представление о тексте:
Определение 1 В хвостовых выражениях из программы , написанной на схеме ядер определяются индуктивно следующим образом .
Тело лямбда-выражения является хвостовым выражением
Если (if E0 E1 E2)является хвостовым выражением, то оба E1и E2являются хвостовыми выражениями.
Ничто другое не является выражением хвоста.
Определение 2 хвост вызов является хвост выражения , которое является вызовом процедуры.
(хвостовой рекурсивный вызов или, как написано в статье, «вызов собственного хвоста» - это особый случай хвостового вызова, когда процедура вызывается сама.)
Она обеспечивает формальные определения для шести различных «машин» для оценки сердечника схемы, где каждая машина имеет такое же наблюдаемое поведение , за исключением для асимптотического класса пространства сложности , что каждый находится.
Например, после предоставления определений для машин с соответственно: 1. управлением памятью на основе стека, 2. сборкой мусора, но без хвостовых вызовов, 3. сборкой мусора и хвостовыми вызовами, документ продолжает работу с еще более продвинутыми стратегиями управления хранением, такими как 4. «хвостовая рекурсия evlis», в которой нет необходимости сохранять среду во время оценки последнего аргумента подвыражения в хвостовом вызове; 5. сокращать среду замыкания только до свободных переменных этого замыкания; 6. так называемая семантика «безопасный для космоса», как определено Аппелем и Шао .
Чтобы доказать, что машины действительно принадлежат к шести различным классам сложности пространства, в статье для каждой пары сравниваемых машин приводятся конкретные примеры программ, которые будут демонстрировать асимптотическое увеличение пространства на одной машине, но не на другой.
(Перечитывая мой ответ сейчас, я не уверен, смог ли я действительно уловить ключевые моменты статьи Клингера . Но, увы, я не могу посвятить больше времени разработке этого ответа прямо сейчас.)
Многие люди уже объяснили рекурсию здесь. Я хотел бы привести пару соображений о некоторых преимуществах, которые дает рекурсия из книги Риккардо Террелла «Параллелизм в .NET, Современные шаблоны параллельного и параллельного программирования»:
«Функциональная рекурсия - это естественный способ итерации в FP, поскольку она позволяет избежать мутации состояния. Во время каждой итерации новое значение передается в конструктор цикла вместо обновления (мутирования). Кроме того, можно составить рекурсивную функцию, сделав вашу программу более модульной, а также предоставив возможности для использования распараллеливания ".
Вот также некоторые интересные заметки из той же книги о хвостовой рекурсии:
Хвостовая рекурсия - это метод, который преобразует обычную рекурсивную функцию в оптимизированную версию, которая может обрабатывать большие входные данные без каких-либо рисков и побочных эффектов.
П р и м е ч а н и е - Основная причина использования хвостового вызова в качестве оптимизации заключается в улучшении локальности данных, использования памяти и использования кэша. Выполняя оконечный вызов, вызываемый абонент использует то же пространство стека, что и вызывающий. Это уменьшает нагрузку на память. Это незначительно улучшает кэш, потому что та же самая память используется повторно для последующих вызывающих абонентов и может оставаться в кэше, вместо того, чтобы вытеснять более старую строку кэша, чтобы освободить место для новой строки кэша.
Ответы:
Рассмотрим простую функцию, которая добавляет первые N натуральных чисел. (например
sum(5) = 1 + 2 + 3 + 4 + 5 = 15
).Вот простая реализация JavaScript, которая использует рекурсию:
Если бы вы позвонили
recsum(5)
, это то, что интерпретатор JavaScript будет оценивать:Обратите внимание, как каждый рекурсивный вызов должен завершиться до того, как интерпретатор JavaScript начнет выполнять вычисление суммы.
Вот хвостовая рекурсивная версия той же функции:
Вот последовательность событий, которые произошли бы, если бы вы вызвали
tailrecsum(5)
(что будет эффективноtailrecsum(5, 0)
из-за второго аргумента по умолчанию).В случае хвостовой рекурсии, с каждой оценкой рекурсивного вызова,
running_total
обновляется.Примечание: в оригинальном ответе использованы примеры из Python. Они были изменены на JavaScript, поскольку интерпретаторы Python не поддерживают оптимизацию хвостового вызова . Однако, хотя оптимизация хвостовых вызовов является частью спецификации ECMAScript 2015 , большинство интерпретаторов JavaScript не поддерживают ее .
источник
tail recursion
этого достичь на языке, который не оптимизирует удаленные вызовы.В традиционной рекурсии типичная модель состоит в том, что вы сначала выполняете свои рекурсивные вызовы, а затем берете возвращаемое значение рекурсивного вызова и вычисляете результат. Таким образом, вы не получите результат своих расчетов, пока не вернетесь после каждого рекурсивного вызова.
В хвостовой рекурсии вы сначала выполняете свои вычисления, а затем выполняете рекурсивный вызов, передавая результаты текущего шага следующему рекурсивному шагу. Это приводит к тому, что последнее утверждение находится в форме
(return (recursive-function params))
. По сути, возвращаемое значение любого заданного рекурсивного шага совпадает с возвращаемым значением следующего рекурсивного вызова .Следствием этого является то, что, как только вы будете готовы выполнить следующий рекурсивный шаг, вам больше не нужен текущий кадр стека. Это учитывает некоторую оптимизацию. На самом деле, с соответствующим письменным компилятором, вы никогда не должны иметь переполнение стека смешок с хвостом рекурсивного вызова. Просто используйте текущий кадр стека для следующего рекурсивного шага. Я почти уверен, что Лисп делает это.
источник
Важным моментом является то, что хвостовая рекурсия по существу эквивалентна зацикливанию. Это не просто вопрос оптимизации компилятора, а фундаментальный факт выразительности. Это идет в обе стороны: вы можете взять любую петлю в форме
где
E
иQ
являются выражениями иS
представляет собой последовательность операторов, и превращают ее в хвостовую рекурсивную функциюКонечно,
E
,S
иQ
должны быть определены , чтобы вычислить некоторые интересные значения по некоторым переменным. Например, функция зацикливанияэквивалентна хвостовой рекурсивной функции
(Это «обертывание» хвостовой рекурсивной функции с функцией с меньшим количеством параметров является обычной функциональной идиомой.)
источник
else { return k; }
можно изменить наreturn k;
Этот отрывок из книги « Программирование на Lua» показывает, как сделать правильную рекурсию хвоста (на Lua, но должна применяться и к Lisp) и почему это лучше.
Итак, вы видите, когда вы делаете рекурсивный вызов вроде:
Это не хвостовая рекурсия, потому что у вас еще есть чем заняться (добавить 1) в этой функции после выполнения рекурсивного вызова. Если вы введете очень большое число, это, вероятно, приведет к переполнению стека.
источник
Используя обычную рекурсию, каждый рекурсивный вызов помещает другую запись в стек вызовов. Когда рекурсия завершена, приложение должно вытолкнуть каждую запись обратно вниз.
При использовании хвостовой рекурсии, в зависимости от языка, компилятор может свернуть стек до одной записи, поэтому вы экономите место в стеке ... Большой рекурсивный запрос может фактически вызвать переполнение стека.
В основном рекурсии Tail могут быть оптимизированы в итерацию.
источник
В файле жаргона есть это, чтобы сказать об определении хвостовой рекурсии:
хвостовая рекурсия / n ./
Если вы уже не устали от этого, посмотрите хвостовую рекурсию.
источник
Вместо объяснения словами, вот пример. Это версия Scheme факториальной функции:
Вот версия факториала с хвостовой рекурсией:
В первой версии вы заметите, что рекурсивный вызов факта подается в выражение умножения, и поэтому при выполнении рекурсивного вызова состояние должно быть сохранено в стеке. В хвостовой рекурсивной версии нет другого S-выражения, ожидающего значение рекурсивного вызова, и, поскольку дальнейших действий не требуется, состояние не нужно сохранять в стеке. Как правило, хвостовые рекурсивные функции Scheme используют постоянное пространство стека.
источник
list-reverse
будет выполняться в пространстве постоянного стека, но создаст и увеличит структуру данных в куче. Обход дерева может использовать моделируемый стек в качестве дополнительного аргумента. и т. д.Хвостовая рекурсия относится к рекурсивному вызову, который является последним в последней логической инструкции в рекурсивном алгоритме.
Обычно в рекурсии у вас есть базовый случай, который останавливает рекурсивные вызовы и начинает выталкивать стек вызовов. Чтобы использовать классический пример, хотя и больше C-ish, чем Lisp, функция факториала иллюстрирует хвостовую рекурсию. Рекурсивный вызов происходит после проверки условия базового случая.
Первоначальный вызов факториала был бы
factorial(n)
гдеfac=1
(значение по умолчанию), а n - это число, для которого нужно рассчитать факториал.источник
else
- это шаг, который вы можете назвать «базовым случаем», но он охватывает несколько строк. Я вас неправильно понимаю или мое предположение верно? Хвостовая рекурсия хороша только для одного лайнера?factorial
Пример просто классический простой пример, это все.Это означает, что вместо необходимости помещать указатель инструкций в стек, вы можете просто перейти к вершине рекурсивной функции и продолжить выполнение. Это позволяет функциям возвращаться бесконечно, не переполняя стек.
Я написал пост в блоге на эту тему, в котором есть графические примеры того, как выглядят фреймы стека.
источник
Вот быстрый фрагмент кода, сравнивающий две функции. Первый - это традиционная рекурсия для поиска факториала заданного числа. Второй использует хвостовую рекурсию.
Очень просто и интуитивно понятно.
Простой способ определить, является ли рекурсивная функция хвостовой рекурсивной, - это если она возвращает конкретное значение в базовом случае. Это означает, что он не возвращает 1 или true или что-то подобное. Скорее всего, он вернет какой-то вариант одного из параметров метода.
Другой способ - определить, свободен ли рекурсивный вызов от каких-либо сложений, арифметических действий, модификаций и т. Д. Это означает, что это не что иное, как чистый рекурсивный вызов.
источник
Лучший способ для меня понять
tail call recursion
- это особый случай рекурсии, когда последний вызов (или хвостовой вызов) - это сама функция.Сравнение примеров, представленных в Python:
^ RECURSION
^ ХВОСТ РЕКУРСИЯ
Как вы можете видеть , в общем рекурсивной версии, окончательный вызов в блоке кода является
x + recsum(x - 1)
. Таким образом, после вызоваrecsum
метода есть еще одна операцияx + ..
.Однако в хвостовой рекурсивной версии последний вызов (или хвостовой вызов) в блоке кода
tailrecsum(x - 1, running_total + x)
означает, что последний вызов сделан самому методу, и после этого никакой операции не происходит.Этот момент важен, потому что хвостовая рекурсия, как видно здесь, не заставляет память расти, потому что, когда базовая виртуальная машина видит функцию, вызывающую себя в хвостовой позиции (последнее выражение, которое должно быть оценено в функции), она устраняет текущий кадр стека, который известен как Оптимизация Tail Call (TCO).
РЕДАКТИРОВАТЬ
NB. Помните, что приведенный выше пример написан на Python, среда выполнения которого не поддерживает TCO. Это всего лишь пример, чтобы объяснить суть. TCO поддерживается на таких языках, как Scheme, Haskell и т. Д.
источник
В Java возможна хвостовая рекурсивная реализация функции Фибоначчи:
Сравните это со стандартной рекурсивной реализацией:
источник
iter
кacc
когдаiter < (n-1)
.Я не программист на Лиспе, но думаю, это поможет.
По сути, это стиль программирования, при котором рекурсивный вызов - это последнее, что вы делаете.
источник
Вот пример Common Lisp, который выполняет факториалы с использованием хвостовой рекурсии. Из-за природы без стека, можно выполнять безумно большие факторные вычисления ...
А потом для удовольствия вы можете попробовать
(format nil "~R" (! 25))
источник
Короче говоря, хвостовая рекурсия имеет рекурсивный вызов как последний оператор в функции, так что ей не нужно ждать рекурсивного вызова.
Так что это хвостовая рекурсия, т. Е. N (x - 1, p * x) - это последний оператор в функции, где компилятор умен, чтобы выяснить, что его можно оптимизировать для цикла for (factorial). Второй параметр p несет значение промежуточного продукта.
Это нерекурсивный способ написания вышеупомянутой факториальной функции (хотя некоторые компиляторы C ++ в любом случае могут ее оптимизировать).
но это не так
Я написал длинный пост под названием « Понимание рекурсии хвоста - Visual Studio C ++ - представление сборки »
источник
Вот версия Perl 5
tailrecsum
функции, упомянутой ранее.источник
Это выдержка из структуры и интерпретации компьютерных программ о хвостовой рекурсии.
источник
Рекурсивная функция - это функция, которая вызывает сама
Это позволяет программистам писать эффективные программы, используя минимальный объем кода .
Недостатком является то, что они могут вызвать бесконечные циклы и другие неожиданные результаты, если они не записаны должным образом .
Я объясню и простую рекурсивную функцию, и хвостовую рекурсивную функцию
Для того, чтобы написать простую рекурсивную функцию
Из приведенного примера:
Из приведенного выше примера
Является ли решающим фактором, когда выйти из цикла
Фактическая обработка должна быть сделана
Позвольте мне решить задачу один за другим для облегчения понимания.
Давайте посмотрим, что произойдет внутри, если я бегу
fact(4)
If
цикл завершается неудачно, поэтому он переходит вelse
цикл, поэтому он возвращает4 * fact(3)
В стековой памяти мы имеем
4 * fact(3)
Подставляя n = 3
If
цикл завершается неудачно, поэтому он переходит вelse
циклтак что возвращается
3 * fact(2)
Помните, мы назвали `` `4 * fact (3)` `
Выход для
fact(3) = 3 * fact(2)
Пока стек имеет
4 * fact(3) = 4 * 3 * fact(2)
В стековой памяти мы имеем
4 * 3 * fact(2)
Подставляя n = 2
If
цикл завершается неудачно, поэтому он переходит вelse
циклтак что возвращается
2 * fact(1)
Помните, мы звонили
4 * 3 * fact(2)
Выход для
fact(2) = 2 * fact(1)
Пока стек имеет
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
В стековой памяти мы имеем
4 * 3 * 2 * fact(1)
Подставляя n = 1
If
петля вернатак что возвращается
1
Помните, мы звонили
4 * 3 * 2 * fact(1)
Выход для
fact(1) = 1
Пока стек имеет
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Наконец, результат факта (4) = 4 * 3 * 2 * 1 = 24
Хвостовая рекурсия будет
If
цикл завершается неудачно, поэтому он переходит вelse
цикл, поэтому он возвращаетfact(3, 4)
В стековой памяти мы имеем
fact(3, 4)
Подставляя n = 3
If
цикл завершается неудачно, поэтому он переходит вelse
циклтак что возвращается
fact(2, 12)
В стековой памяти мы имеем
fact(2, 12)
Подставляя n = 2
If
цикл завершается неудачно, поэтому он переходит вelse
циклтак что возвращается
fact(1, 24)
В стековой памяти мы имеем
fact(1, 24)
Подставляя n = 1
If
петля вернатак что возвращается
running_total
Выход для
running_total = 24
Наконец, результат факта (4,1) = 24
источник
Хвостовая рекурсия - это жизнь, которой вы живете сейчас. Вы постоянно перерабатываете один и тот же кадр стека, снова и снова, потому что нет причин или средств для возврата к «предыдущему» кадру. С прошлым покончено, и с ним можно отказаться. Вы получаете один кадр, навсегда двигаясь в будущее, пока ваш процесс неизбежно не умрет.
Аналогия нарушается, если учесть, что некоторые процессы могут использовать дополнительные кадры, но все равно считаются хвостово-рекурсивными, если стек не растет бесконечно.
источник
Рассмотрим проблему вычисления факториала числа.
Прямой подход будет:
Предположим, вы звоните факториалом (4). Дерево рекурсии будет:
Максимальная глубина рекурсии в приведенном выше случае составляет O (n).
Однако рассмотрим следующий пример:
Дерево рекурсии для factTail (4) будет:
Здесь также максимальная глубина рекурсии равна O (n), но ни один из вызовов не добавляет никакой дополнительной переменной в стек. Следовательно, компилятор может покончить со стеком.
источник
Хвостовая рекурсия довольно быстрая по сравнению с обычной рекурсией. Это быстро, потому что вывод вызова предков не будет записан в стек, чтобы сохранить трек. Но в обычной рекурсии все предки вызывают вывод, записанный в стеке, чтобы сохранить трек.
источник
Хвост рекурсивная функция является рекурсивной функцией , где последняя операция это делает , прежде чем Возвращаться сделать вызов рекурсивной функции. То есть возвращаемое значение рекурсивного вызова функции сразу возвращается. Например, ваш код будет выглядеть так:
Компиляторы и интерпретаторы, которые реализуют оптимизацию хвостовых вызовов или устранение хвостовых вызовов, могут оптимизировать рекурсивный код для предотвращения переполнения стека. Если ваш компилятор или интерпретатор не реализует оптимизацию хвостовых вызовов (например, интерпретатор CPython), то нет никакой дополнительной выгоды для написания вашего кода таким способом.
Например, это стандартная рекурсивная факториальная функция в Python:
И это рекурсивная версия хвостового вызова факториальной функции:
(Обратите внимание, что, хотя это и есть код Python, интерпретатор CPython не выполняет оптимизацию хвостовых вызовов, поэтому такая организация вашего кода не дает никаких преимуществ во время выполнения.)
Возможно, вам придется сделать свой код немного более нечитаемым, чтобы использовать оптимизацию хвостовых вызовов, как показано в примере факториала. (Например, базовый случай теперь немного неинтуитивен, и
accumulator
параметр эффективно используется как своего рода глобальная переменная.)Но преимущество оптимизации хвостового вызова состоит в том, что она предотвращает ошибки переполнения стека. (Я отмечу, что вы можете получить то же преимущество, используя итеративный алгоритм вместо рекурсивного.)
Переполнения стека возникают, когда в стеке вызовов слишком много объектов кадра. Объект кадра помещается в стек вызовов при вызове функции и выталкивается из стека вызовов при возврате функции. Объекты Frame содержат информацию, такую как локальные переменные и строку кода, к которой нужно вернуться, когда функция вернется.
Если ваша рекурсивная функция делает слишком много рекурсивных вызовов без возврата, стек вызовов может превысить свой предел объекта фрейма. (Число зависит от платформы; в Python по умолчанию это 1000 кадровых объектов.) Это вызывает переполнение стека ошибку . (Эй, отсюда и название этого сайта!)
Однако, если последнее, что делает ваша рекурсивная функция, это делает рекурсивный вызов и возвращает его возвращаемое значение, то нет никаких причин для того, чтобы поддерживать текущий объект фрейма в стеке вызовов. В конце концов, если после рекурсивного вызова функции нет кода, нет причин зависать от локальных переменных текущего фрейма. Таким образом, мы можем немедленно избавиться от текущего объекта фрейма, а не сохранять его в стеке вызовов. Конечным результатом этого является то, что ваш стек вызовов не увеличивается в размере и, следовательно, не может переполняться стеком.
Компилятор или интерпретатор должен иметь функцию хвостового вызова в качестве функции, чтобы он мог распознавать, когда может быть применена оптимизация хвостового вызова. Даже в этом случае вы могли бы переупорядочить код в своей рекурсивной функции, чтобы использовать оптимизацию хвостовых вызовов, и вам решать, стоит ли это потенциальное снижение читабельности оптимизации.
источник
Чтобы понять некоторые основные различия между рекурсией хвостового вызова и рекурсией не хвостового вызова, мы можем изучить реализации этих технологий в .NET.
Вот статья с некоторыми примерами на C #, F # и C ++ \ CLI: Приключения в Tail Recursion на C #, F # и C ++ \ CLI .
C # не оптимизирует для рекурсии хвостового вызова, тогда как F # делает.
Принципиальные различия включают в себя циклы и лямбда-исчисление. C # разработан с учетом циклов, тогда как F # построен на принципах лямбда-исчисления. За очень хорошую (и бесплатную) книгу о принципах лямбда-исчисления см. « Структура и интерпретация компьютерных программ» Абельсона, Суссмана и Суссмана .
Относительно хвостовых вызовов в F #, для очень хорошей вводной статьи, см. Подробное введение в хвостовые вызовы в F # . Наконец, вот статья, в которой рассматривается различие между рекурсией без хвоста и рекурсией с использованием хвостового вызова (в F #): рекурсия с хвостом против рекурсии без хвоста в F sharp .
Если вы хотите прочитать о некоторых конструктивных различиях рекурсии хвостового вызова между C # и F #, см. Генерация кода операции Tail-Call в C # и F # .
Если вам нужно знать, какие условия мешают компилятору C # выполнять оптимизацию хвостового вызова, см. Эту статью: Условия хвостового вызова JIT CLR .
источник
Существует два основных вида рекурсии: рекурсия головы и хвостовая рекурсия.
Взято из этого супер классного поста. Пожалуйста, подумайте над прочтением.
источник
Рекурсия означает функцию, вызывающую себя. Например:
Tail-Recursion означает рекурсию, завершающую функцию:
Видите, последнее, что делает незавершенная функция (процедура, на языке жаргона Scheme), - это вызывает себя. Другой (более полезный) пример:
В вспомогательной процедуре последнее, что она делает, если left не равно nil, - это вызывает себя (ПОСЛЕ того, что что-то минует и что-то cdr). Это в основном, как вы отображаете список.
Хвостовая рекурсия имеет большое преимущество в том, что интерпретатор (или компилятор, зависящий от языка и поставщика) может оптимизировать его и преобразовать в нечто, эквивалентное циклу while. На самом деле, в традиции Scheme, большинство циклов for и while выполняется методом хвостовой рекурсии (насколько я знаю, нет и for, и while).
источник
На этот вопрос есть много хороших ответов ... но я не могу не согласиться с альтернативным подходом к определению «рекурсии хвоста» или, по крайней мере, «правильной рекурсии хвоста». А именно: следует ли рассматривать его как свойство определенного выражения в программе? Или следует рассматривать это как свойство реализации языка программирования ?
Более подробно о последнем представлении есть классическая статья Уилла Клингера «Правильная рекурсия хвоста и эффективность использования пространства» (PLDI 1998), в которой «правильная хвостовая рекурсия» определяется как свойство реализации языка программирования. Определение построено так, чтобы позволить игнорировать подробности реализации (например, представлен ли стек вызовов на самом деле через стек времени выполнения или через выделенный в куче связанный список кадров).
Для этого используется асимптотический анализ: не времени выполнения программы, как обычно, а скорее использования пространства программы . Таким образом, использование пространства распределенного списка, выделенного в куче, по сравнению со стеком вызовов времени выполнения оказывается асимптотически эквивалентным; таким образом, можно игнорировать эту деталь реализации языка программирования (деталь, которая, безусловно, имеет большое значение на практике, но может немного испачкать воду, когда кто-то пытается определить, удовлетворяет ли данная реализация требованию «рекурсивности хвоста свойства»). )
Статья заслуживает тщательного изучения по ряду причин:
Он дает индуктивное определение хвостовых выражений и хвостовых вызовов программы. (Такое определение и почему такие звонки важны, по-видимому, является предметом большинства других ответов, приведенных здесь.)
Вот эти определения, просто чтобы дать представление о тексте:
(хвостовой рекурсивный вызов или, как написано в статье, «вызов собственного хвоста» - это особый случай хвостового вызова, когда процедура вызывается сама.)
Она обеспечивает формальные определения для шести различных «машин» для оценки сердечника схемы, где каждая машина имеет такое же наблюдаемое поведение , за исключением для асимптотического класса пространства сложности , что каждый находится.
Например, после предоставления определений для машин с соответственно: 1. управлением памятью на основе стека, 2. сборкой мусора, но без хвостовых вызовов, 3. сборкой мусора и хвостовыми вызовами, документ продолжает работу с еще более продвинутыми стратегиями управления хранением, такими как 4. «хвостовая рекурсия evlis», в которой нет необходимости сохранять среду во время оценки последнего аргумента подвыражения в хвостовом вызове; 5. сокращать среду замыкания только до свободных переменных этого замыкания; 6. так называемая семантика «безопасный для космоса», как определено Аппелем и Шао .
Чтобы доказать, что машины действительно принадлежат к шести различным классам сложности пространства, в статье для каждой пары сравниваемых машин приводятся конкретные примеры программ, которые будут демонстрировать асимптотическое увеличение пространства на одной машине, но не на другой.
(Перечитывая мой ответ сейчас, я не уверен, смог ли я действительно уловить ключевые моменты статьи Клингера . Но, увы, я не могу посвятить больше времени разработке этого ответа прямо сейчас.)
источник
Многие люди уже объяснили рекурсию здесь. Я хотел бы привести пару соображений о некоторых преимуществах, которые дает рекурсия из книги Риккардо Террелла «Параллелизм в .NET, Современные шаблоны параллельного и параллельного программирования»:
Вот также некоторые интересные заметки из той же книги о хвостовой рекурсии:
источник