Ответ 1
Хотя вы часто видите следующее в качестве примера преобразования факториала в tail-call:
int factorial(int n, int acc=1) {
if (n <= 1) return acc;
else return factorial(n-1, n*acc);
}
это не совсем правильно, так как это требует, чтобы умножение было как ассоциативным, так и коммутативным. (Умножение ассоциативно и коммутативно, но приведенное выше не служит моделью для других операций, которые не удовлетворяют этим ограничениям.) Лучшее решение может быть:
int factorial(int n, int k=1, int acc=1) {
if (n == 0) return acc;
else return factorial(n-1, k+1, acc*k);
}
Это также служит моделью для преобразования фибоначчи:
int fibonacci(int n, int a=1, int b=0) {
if (n == 0) return a;
else return fibonacci(n-1, a+b, a);
}
Обратите внимание, что они вычисляют последовательность, начинающуюся с начала, в отличие от ожидающих продолжения очередей в стеке вызовов. Поэтому они структурно больше похожи на итеративное решение, чем на рекурсивное решение. В отличие от итеративной программы, они никогда не изменяют какую-либо переменную; все привязки постоянны. Это интересное и полезное свойство; в этих простых случаях это не имеет особого значения, но писать код без переназначений упрощает оптимизацию компилятора.
Во всяком случае, последний дает модель для вашей рекурсивной функции; как последовательность фибоначчи, нам нужно сохранить соответствующие прошлые значения, но нам нужно три из них вместо двух:
int mouse(int n, int a=1, int b=1, int c=1) {
if (n <=2 ) return a;
else return mouse(n-1, a*c+1, a, b);
}
Addenda
В комментариях были подняты два вопроса. Я попытаюсь ответить на них (и еще один) здесь.
Во-первых, должно быть ясно (из соображений базовой машинной архитектуры, которая не имеет понятия вызова функции), что любой вызов функции может быть перефразирован как goto (возможно, с не ограниченным промежуточным хранилищем); кроме того, любой goto может быть выражен как хвостовой вызов. Таким образом, возможно (но не обязательно красиво) переписать любую рекурсию как хвостовую рекурсию.
Обычный механизм - это стиль продолжения-прохода, который является причудливым способом сказать, что каждый раз, когда вы хотите вызвать функцию, вместо этого вы добавляете остальную часть текущей функции в новую функцию ( "продолжение" ) и передать это продолжение вызываемой функции. Поскольку каждая функция затем получает продолжение в качестве аргумента, она должна завершить любое продолжение, которое она создает, с вызовом полученного им продолжения.
Это, вероятно, достаточно, чтобы заставить голову вращаться, поэтому я скажу иначе: вместо того, чтобы вставлять аргументы и возвращаемое местоположение в стек и вызывать функцию (которая позже будет возвращена), вы нажимаете аргументы и место продолжения в стек и перейти к функции, которая позже будет продолжена. Короче говоря, вы просто делаете стек явным параметром, и вам никогда не нужно возвращаться. Этот стиль программирования распространен в управляемом событиями коде (см. Python Twisted), и это настоящая боль, чтобы писать (и читать). Поэтому я настоятельно рекомендую разрешить компиляторам сделать это преобразование для вас, если вы найдете тот, который сделает это.
@xxmouse предположил, что я вытащил уравнение рекурсии из шляпы и спросил, как она была получена. Это просто оригинальная рекурсия, но переформулирована как преобразование одного кортежа:
fn = fn-1*fn-3 + 1
=>
Fn = <Fn-11*Fn-13+1, Fn-11, Fn-12>
Я не знаю, ясно ли это, но это лучшее, что я могу сделать. Посмотрите пример фибоначчи на несколько более простой случай.
@j_random_hacker спрашивает, каковы пределы этого преобразования. Он работает для рекурсивной последовательности, где каждый элемент может быть выражен некоторой формулой предыдущих элементов k
, где k
является константой. Существуют и другие способы создания рекурсии хвостового вызова. Например:
// For didactic purposes only
bool is_odd(int n) { return n%2 == 1; }
int power(int x, int n, int acc=1) {
if (n == 0) return acc;
else if (is_odd(n)) return power(x, n-1, acc*x);
else return power(x*x, n/2, acc);
}
Вышеупомянутое не совпадает с обычной рекурсией без хвоста, которая выполняет другую (но эквивалентную и одинаково длинную) последовательность умножений.
int squared(n) { return n * n; }
int power(int x, int n) {
if (n == 0) return 1;
else if (is_odd(n)) return x * power(x, n-1));
else return squared(power(x, n/2));
}
Спасибо Алексею Фрунзе за следующий тест: Выход (ideone):
mouse(0) = 1
mouse(1) = 1
mouse(2) = 1
mouse(3) = 2
mouse(4) = 3
mouse(5) = 4
mouse(6) = 9
mouse(7) = 28
mouse(8) = 113
mouse(9) = 1018