Что такое хвостовая рекурсия?

В то время как я начинаю изучать lisp, я сталкивался с термином tail-recursive. Что это значит?

1247
задан Ben Lever 29 авг. '08 в 6:48
источник поделиться
22 ответов

Рассмотрим простую функцию, которая добавляет первые 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-интерпретатор:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15

Обратите внимание, что каждый рекурсивный вызов должен быть выполнен до того, как интерпретатор JavaScript начнет фактически выполнять вычисление суммы.

Здесь хвосто-рекурсивная версия той же функции:

function tailrecsum(x, running_total=0) {
    if (x===0) {
        return running_total;
    } else {
        return tailrecsum(x-1, running_total+x);
    }
}

Здесь последовательность событий, которые произойдут, если вы вызвали tailrecsum(5) (что эффективно было бы tailrecsum(5, 0) из-за второго аргумента по умолчанию).

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

В хвостохранилище с каждой оценкой рекурсивного вызова обновляется running_total.

Примечание. Исходный ответ использовал примеры из Python. Они были изменены на JavaScript, поскольку современные интерпретаторы JavaScript поддерживают оптимизацию хвостовых вызовов, но интерпретаторы Python этого не делают.

1241
ответ дан Lorin Hochstein 31 авг. '08 в 21:21
источник поделиться

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

В рекурсии хвоста сначала выполняете свои вычисления, а затем выполняете рекурсивный вызов, передавая результаты текущего шага на следующий рекурсивный шаг. Это приводит к тому, что последний оператор находится в форме "(return (параметры рекурсивной функции))" (я думаю, что синтаксис для Lisp). В принципе, возвращаемое значение любого заданного рекурсивного шага совпадает с возвращаемым значением следующего рекурсивного вызова.

Следствием этого является то, что, как только вы будете готовы выполнить свой следующий рекурсивный шаг, вам больше не нужен текущий стек кадров. Это позволяет оптимизировать. Фактически, с соответствующим образом написанным компилятором, у вас никогда не должно быть сверкающего с хвостовым рекурсивным вызовом. Просто повторите использование текущего кадра стека для следующего рекурсивного шага. Я уверен, что Lisp делает это.

531
ответ дан user316 29 авг. '08 в 6:57
источник поделиться

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

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

эквивалентно хвостовой рекурсивной функции (s)

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

(Эта "обертка" хвостовой рекурсивной функции с функцией с меньшим количеством параметров является общей функциональной идиомой.)

160
ответ дан Chris Conway 31 авг. '08 в 20:29
источник поделиться

В этой выдержке из книги "Программирование в Луа" показано как сделать правильную рекурсию хвоста (в Lua, но она также должна применяться к Lisp ) и почему это лучше.

Хвост вызова [хвостовая рекурсия] - это своего рода переодетый как вызов. Вызов хвоста происходит, когда функция вызывает другую как свою последнюю действия, поэтому ему больше нечего делать. Например, в следующем коде, вызов g - это хвостовой вызов:

function f (x)
  return g(x)
end

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

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

function foo (n)
  if n > 0 then return foo(n - 1) end
end

... Как я сказал ранее, хвостовой вызов - это вид goto. Таким образом, весьма полезный применение правильных хвостовых вызовов в Lua предназначен для программирования государственных машин. Такие приложения могут представлять каждый состояние по функции; изменить состояние заключается в том, чтобы перейти к (или вызвать) конкретную функция. В качестве примера давайте рассмотрим простую игру в лабиринте. Лабиринт имеет несколько комнат, каждый с четыре двери: север, юг, восток и запад. На каждом шаге пользователь вводит направление движения. Если есть дверь в этом направлении пользователь переходит к соответствующая комната; в противном случае программа выводит предупреждение. Целью является перейти от начальной комнаты к финальной номер.

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

function room1 ()
  local move = io.read()
  if move == "south" then return room3()
  elseif move == "east" then return room2()
  else print("invalid move")
       return room1()   -- stay in the same room
  end
end

function room2 ()
  local move = io.read()
  if move == "south" then return room4()
  elseif move == "west" then return room1()
  else print("invalid move")
       return room2()
  end
end

function room3 ()
  local move = io.read()
  if move == "north" then return room1()
  elseif move == "east" then return room4()
  else print("invalid move")
       return room3()
  end
end

function room4 ()
  print("congratulations!")
end

Итак, вы видите, когда вы делаете рекурсивный вызов типа:

function x(n)
  if n==0 then return 0
  n= n-2
  return x(n) + 1
end

Это не хвостовая рекурсия, потому что у вас есть все, что нужно сделать (добавить 1) в эту функцию после рекурсивного вызова. Если вы вводите очень большое число, это, вероятно, вызовет переполнение стека.

106
ответ дан Hoffmann 29 авг. '08 в 19:03
источник поделиться

Вместо того, чтобы объяснять это словами, вот пример. Это схема факториальной функции:

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

55
ответ дан Kyle Cronin 29 авг. '08 в 6:57
источник поделиться

Используя регулярную рекурсию, каждый рекурсивный вызов подталкивает другую запись в стек вызовов. Когда рекурсия завершена, приложение затем должно вытолкнуть каждую запись полностью назад.

С рекурсией хвоста компилятор может свернуть стек до одной записи, поэтому вы сохраняете пространство стека... Большой рекурсивный запрос может фактически вызвать переполнение стека.

В основном рекурсии хвоста могут быть оптимизированы в итерации.

53
ответ дан FlySwat 29 авг. '08 в 6:55
источник поделиться

В файле жаргона говорится об определении хвостовой рекурсии:

рекурсия хвоста/n./

Если вы уже не устали от этого, см. рекурсию хвоста.

51
ответ дан Pat 29 авг. '08 в 10:21
источник поделиться

Рекурсия хвоста означает, что рекурсивный вызов является последним в последней логической инструкции в рекурсивном алгоритме.

Как правило, в рекурсии у вас есть базовый регистр, который останавливает рекурсивные вызовы и начинает выскакивать стек вызовов. Чтобы использовать классический пример, хотя больше C-ish, чем Lisp, факториальная функция иллюстрирует рекурсию хвоста. Рекурсивный вызов возникает после проверки базового условия.

factorial(x, fac) {
  if (x == 1)
     return fac;
   else
     return factorial(x-1, x*fac);
}

Обратите внимание, что начальный вызов факториала должен быть факториальным (n, 1), где n - это число, для которого нужно вычислить факториал.

21
ответ дан Peter Meyer 29 авг. '08 в 6:57
источник поделиться

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

Я написал сообщение blog по теме, в котором есть графические примеры того, как выглядят фреймы стека.

20
ответ дан Chris Smith 01 сент. '08 в 2:52
источник поделиться

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

Очень простой и интуитивно понятный.

Легкий способ определить, является ли рекурсивная функция хвостовой рекурсивной, - если она возвращает конкретное значение в базовом случае. Это означает, что он не возвращает 1 или true или что-то в этом роде. Это, скорее всего, вернет некоторый вариант одного из параметров метода.

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

public static int factorial(int mynumber) {
    if (mynumber == 1) {
        return 1;
    } else {            
        return mynumber * factorial(--mynumber);
    }
}

public static int tail_factorial(int mynumber, int sofar) {
    if (mynumber == 1) {
        return sofar;
    } else {
        return tail_factorial(--mynumber, sofar * mynumber);
    }
}
12
ответ дан AbuZubair 19 дек. '10 в 18:52
источник поделиться

В Java здесь можно найти хвостовую рекурсивную реализацию функции Фибоначчи:

public int tailRecursive(final int n) {
    if (n <= 2)
        return 1;
    return tailRecursiveAux(n, 1, 1);
}

private int tailRecursiveAux(int n, int iter, int acc) {
    if (iter == n)
        return acc;
    return tailRecursiveAux(n, ++iter, acc + iter);
}

Сравните это со стандартной рекурсивной реализацией:

public int recursive(final int n) {
    if (n <= 2)
        return 1;
    return recursive(n - 1) + recursive(n - 2);
}
10
ответ дан jorgetown 15 окт. '08 в 0:20
источник поделиться

вот версия 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
}
7
ответ дан Brad Gilbert 02 окт. '08 в 1:06
источник поделиться

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

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

ИЗМЕНИТЬ

NB. Имейте в виду, что приведенный выше пример написан на Python, время выполнения которого не поддерживает TCO. Это просто пример, объясняющий суть дела. TCO поддерживается в таких языках, как Scheme, Haskell и т.д.

7
ответ дан Abhiroop Sarkar 21 дек. '15 в 14:47
источник поделиться

Вот общий пример Lisp, который использует факториалы с использованием хвостовой рекурсии. Из-за отсутствия стека можно было выполнить безумно большие факториальные вычисления...

(defun ! (n &optional (product 1))
    (if (zerop n) product
        (! (1- n) (* product n))))

А затем для удовольствия вы можете попробовать (format nil "~R" (! 25))

7
ответ дан user922475 11 марта '12 в 9:07
источник поделиться

Короче говоря, хвостовая рекурсия имеет рекурсивный вызов в качестве оператора last в функции, поэтому ему не нужно ждать рекурсивного вызова.

Итак, это хвостовая рекурсия, т.е. N (x - 1, p * x) является последним оператором в функции, где компилятор умен, чтобы понять, что он может быть оптимизирован для цикла (факториала). Второй параметр p несет промежуточное значение продукта.

function N(x, p) {
   return x == 1 ? p : N(x - 1, p * x);
}

Это нерекурсивный способ записи вышеупомянутой факториальной функции (хотя некоторые компиляторы С++ могут в любом случае ее оптимизировать).

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

Я написал длинный пост под заголовком " Общие сведения о рекурсии хвоста - Visual Studio С++ - Сборочный вид"

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

6
ответ дан Zhihua Lai 09 нояб. '16 в 2:21
источник поделиться

Я не программист Lisp, но я думаю, это поможет.

В основном это стиль программирования, так что рекурсивный вызов - это последнее, что вы делаете.

6
ответ дан Matt Hamilton 29 авг. '08 в 6:50
источник поделиться

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

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

4
ответ дан user633183 23 дек. '16 в 21:04
источник поделиться

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

Вот статья с некоторыми примерами в С#, F # и С++\CLI: Приключения в рекурсии хвоста в С#, F # и С++\CLI.

С# не оптимизируется для рекурсии хвостового вызова, тогда как F # делает.

Различия в принципе связаны с циклами против лямбда-исчисления. С# спроектирован с учетом циклов, в то время как F # построен из принципов исчисления лямбда. Для очень хорошей (и свободной) книги на принципах лямбда-исчисления см.: Структура и интерпретация компьютерных программ, Абельсон, Суссман и Суссман.

Что касается хвостовых вызовов в F #, то для очень хорошей вводной статьи см. Подробное введение в Tail Calls в F #. Наконец, вот статья, которая описывает разницу между нерегулярной рекурсией и рекурсией хвостового вызова (в F #): Рекурсия хвоста и нерегулярной рекурсии в F sharp.

Если вы хотите прочитать о некоторых различиях в дизайне рекурсии хвостового вызова между С# и F #, см.: Генерирование кода хвоста в С# и F #.

Если вам все равно, чтобы узнать, какие условия не позволяют компилятору С# выполнять оптимизацию хвостовых вызовов, см. эту статью: Условия хвостового вызова JIT CLR.

4
ответ дан devinbost 28 апр. '14 в 22:13
источник поделиться

Это выдержка из Структура и интерпретация компьютерных программ о рекурсии хвоста.

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

Одна из причин того, что различие между процессом и процедурой может быть смущает то, что большинство реализаций общих языков (включая Ada, Pascal и C) разработаны таким образом, что интерпретация любых рекурсивных процедура потребляет объем памяти, который растет с количеством вызовы процедур, даже если описанный процесс, в принципе, итеративный. Как следствие, эти языки могут описывать итеративные процессов, только прибегая к специальным "петлевым конструкциям", такие как do, repeat, until, for и while. Реализация Схема не разделяет этот дефект. Это будет выполнять итерационный процесс в постоянном пространстве, даже если итерационный процесс описывается рекурсивной процедурой. реализация с этим свойством называется tail-recursive.. рекурсивная реализация, итерация может быть выражена с помощью обычный механизм вызова процедуры, так что специальная итерация конструкции полезны только как синтаксический сахар.

3
ответ дан ayushgp 27 июня '16 в 12:41
источник поделиться

В этом вопросе есть много отличных ответов... но я не могу не ответить на альтернативный подход, как определить "рекурсию хвоста" или, по крайней мере, "правильную рекурсию хвоста". А именно: следует ли рассматривать его как свойство определенного выражения в программе? Или следует рассматривать его как свойство реализации языка программирования?

Более подробно о последнем представлении есть классический paper Will Clinger, "Надлежащая рекурсия хвоста и эффективность пространства" (PLDI 1998), которая определила "правильную рекурсию хвоста" как свойство реализации языка программирования. Определение сконструировано так, чтобы можно было игнорировать детали реализации (например, действительно ли стек вызовов фактически представлен через стек времени выполнения или через связанный с кучей связанный список кадров).

Чтобы достичь этого, он использует асимптотический анализ: не время выполнения программы, как обычно видит, а скорее использование программного пространства. Таким образом, использование пространства связанного с кучей списка ссылок и стека вызовов времени выполнения оказывается асимптотически эквивалентным; поэтому можно игнорировать эту деталь реализации реализации языка программирования (деталь, которая, безусловно, имеет значение практически на практике, но может немного загрязнять воды, когда вы пытаетесь определить, удовлетворяет ли данная реализация требованию "рекурсивный хвост свойства" ) )

Статья заслуживает тщательного изучения по ряду причин:

  • Он дает индуктивное определение хвостовых выражений и хвостовых вызовов программы. (Такое определение и почему такие призывы важны, кажется, являются предметом большинства других ответов, приведенных здесь.)

    Вот эти определения, просто чтобы обеспечить вкус текста:

    Определение 1. Выражения хвоста программы, записанные в Core Scheme, определяются индуктивно следующим образом.

    • Тело выражения лямбда - это выражение хвоста
    • Если (if E0 E1 E2) является хвостовым выражением, то и E1, и E2 являются хвостовыми выражениями.
    • Ничто другое не является выражением хвоста.

    Определение 2 Хвост-вызов - это выражение хвоста, которое является вызовом процедуры.

(хвостовой рекурсивный вызов, или, как говорится в документе, "вызов с собственным хвостом" является частным случаем хвостового вызова, в котором сама процедура вызывается).

  • Он предоставляет формальные определения для шести разных "машин" для оценки Core Scheme, где каждая машина имеет одно и то же наблюдаемое поведение, за исключением класса асимптотической сложности пространства, в котором находится каждый.

    Например, после определения определений для машин, соответственно, 1. управление памятью на основе стека, 2. сбор мусора, но отсутствие хвостовых вызовов, 3. сбор мусора и хвостовые звонки, бумага продолжается вперед с еще более продвинутыми стратегиями управления хранением, например 4. "evlis tail recursion", где окружающая среда не нуждается в сохранении во время оценки последнего аргумента подвыражения в хвостовом вызове, 5. сокращение среды замыкания только до бесплатных переменных этого закрытие и 6. так называемая "безопасная для космоса" семантика, как определено Аппель и Шао.

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


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

2
ответ дан pnkfelix 05 июля '17 в 13:51
источник поделиться

Рекурсия означает функцию, вызывающую себя. Например:

(define (un-ended name)
  (un-ended 'me)
  (print "How can I get here?"))

Tail-Recursion означает рекурсию, которая завершает функцию:

(define (un-ended name)
  (print "hello")
  (un-ended 'me))

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

(define (map lst op)
  (define (helper done left)
    (if (nil? left)
        done
        (helper (cons (op (car left))
                      done)
                (cdr left))))
  (reverse (helper '() lst)))

В процедуре помощника, ПОСЛЕДНЯЯ вещь, которую она делает, если левая, не ноль, это называть себя (ПОСЛЕ чего-то и что-то cdr). Это в основном то, как вы отображаете список.

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

2
ответ дан magice 02 сент. '08 в 17:08
источник поделиться

Рекурсия хвоста - это рекурсивная функция, в которой функция вызывает в конце ( "хвост" ) функции, в которой никакое вычисление не является после возвращения рекурсивного вызова. Многие компиляторы оптимизируют изменить рекурсивный вызов на хвостовой рекурсивный или итеративный вызов.

Рассмотрим задачу вычисления факториала числа.

Прямым подходом будет:

  factorial(n):

    if n==0 then 1

    else n*factorial(n-1)

Предположим, вы вызываете факториал (4). Деревом рекурсии было бы следующее:

       factorial(4)
       /        \
      4      factorial(3)
     /             \
    3          factorial(2)
   /                  \
  2                factorial(1)
 /                       \
1                       factorial(0)
                            \
                             1    

Максимальная глубина рекурсии в приведенном выше случае равна O (n).

Однако рассмотрим следующий пример:

factAux(m,n):
if n==0  then m;
else     factAux(m*n,n-1);

factTail(n):
   return factAux(1,n);

Деревом рекурсии для factTail (4) будет:

factTail(4)
   |
factAux(1,4)
   |
factAux(4,3)
   |
factAux(12,2)
   |
factAux(24,1)
   |
factAux(24,0)
   |
  24

Здесь также максимальная глубина рекурсии - O (n), но ни один из вызовов не добавляет в стек дополнительную дополнительную переменную. Следовательно, компилятор может удалить стек.

2
ответ дан coding_ninza 23 дек. '17 в 15:26
источник поделиться