Это зависит от того, насколько строго вы определяете «рекурсию».
Если мы строго требуем, чтобы он задействовал стек вызовов (или какой-либо другой механизм для поддержания состояния программы), то мы всегда можем заменить его чем-то, что этого не делает. Действительно, языки, которые естественным образом приводят к интенсивному использованию рекурсии, обычно имеют компиляторы, которые интенсивно используют оптимизацию хвостовых вызовов, поэтому то, что вы пишете, является рекурсивным, но то, что вы запускаете, является итеративным.
Но давайте рассмотрим случай, когда мы делаем рекурсивный вызов и используем результат рекурсивного вызова для этого рекурсивного вызова.
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
if (m == 0)
return n+1;
if (n == 0)
return Ackermann(m - 1, 1);
else
return Ackermann(m - 1, Ackermann(m, n - 1));
}
Сделать первый рекурсивный вызов итеративным легко:
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
restart:
if (m == 0)
return n+1;
if (n == 0)
{
m--;
n = 1;
goto restart;
}
else
return Ackermann(m - 1, Ackermann(m, n - 1));
}
Затем мы можем убрать, goto
чтобы отогнать велоцирапторы и тень Дейкстры:
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
while(m != 0)
{
if (n == 0)
{
m--;
n = 1;
}
else
return Ackermann(m - 1, Ackermann(m, n - 1));
}
return n+1;
}
Но чтобы удалить другие рекурсивные вызовы, нам нужно будет сохранить значения некоторых вызовов в стек:
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
Stack<BigInteger> stack = new Stack<BigInteger>();
stack.Push(m);
while(stack.Count != 0)
{
m = stack.Pop();
if(m == 0)
n = n + 1;
else if(n == 0)
{
stack.Push(m - 1);
n = 1;
}
else
{
stack.Push(m - 1);
stack.Push(m);
--n;
}
}
return n;
}
Теперь, когда мы рассматриваем исходный код, мы, безусловно, превратили наш рекурсивный метод в итеративный.
Учитывая то, к чему это было скомпилировано, мы превратили код, который использует стек вызовов для реализации рекурсии, в код, который этого не делает (и при этом превратили код, который сгенерирует исключение переполнения стека даже для довольно небольших значений, в код, который будет просто Возьмите мучительно много времени, чтобы вернуться [см. Как я могу предотвратить переполнение стека моей функцией Аккермана? Для дальнейшей оптимизации, которая заставляет его действительно возвращаться для многих других возможных вводов]).
Учитывая то, как рекурсия реализована в целом, мы превратили код, использующий стек вызовов, в код, который использует другой стек для хранения ожидающих операций. Поэтому мы можем утверждать, что он все еще рекурсивен, когда рассматривается на этом низком уровне.
И на этом уровне действительно нет других способов обойти это. Так что, если вы считаете этот метод рекурсивным, то действительно есть вещи, которые мы не можем сделать без него. Вообще, хотя мы не маркируем такой код рекурсивно. Термин рекурсия полезен, потому что он охватывает определенный набор подходов и дает нам возможность поговорить о них, и мы больше не используем один из них.
Конечно, все это предполагает, что у вас есть выбор. Существуют как языки, которые запрещают рекурсивные вызовы, так и языки, в которых отсутствуют циклические структуры, необходимые для итерации.