Zależy to od tego, jak ściśle zdefiniujesz „rekurencję”.
Jeśli bezwzględnie wymagamy, aby obejmował stos wywołań (lub jakikolwiek inny mechanizm służący do utrzymania stanu programu), zawsze możemy zastąpić go czymś, co nie. Rzeczywiście, języki, które naturalnie prowadzą do intensywnego korzystania z rekurencji, zwykle mają kompilatory, które intensywnie korzystają z optymalizacji wywołania ogona, więc to, co piszesz, jest rekurencyjne, ale to, co uruchamiasz, jest iteracyjne.
Rozważmy jednak przypadek, w którym wykonujemy połączenie rekurencyjne i wykorzystujemy wynik połączenia rekurencyjnego dla tego połączenia rekurencyjnego.
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));
}
Wykonanie pierwszej iteracyjnej wywołania rekurencyjnego jest łatwe:
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));
}
Następnie możemy posprzątać, goto
aby odeprzeć welociraptory i cień Dijkstry:
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;
}
Ale aby usunąć inne wywołania rekurencyjne, będziemy musieli przechowywać wartości niektórych wywołań w stosie:
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;
}
Teraz, gdy bierzemy pod uwagę kod źródłowy, z pewnością zmieniliśmy naszą metodę rekurencyjną na iteracyjną.
Biorąc pod uwagę, do czego to zostało skompilowane, zmieniliśmy kod, który używa stosu wywołań, aby zaimplementować rekurencję w kodzie, który tego nie robi (i robiąc to, przekształcił kod, który wyrzuci wyjątek przepełnienia stosu nawet dość małych wartości do kodu, który po prostu powrót zajmuje bardzo dużo czasu [patrz Jak mogę zapobiec przepełnieniu stosu przez moją funkcję Ackermana? w celu uzyskania dalszych optymalizacji, które sprawiają, że faktycznie zwraca on wiele innych możliwych danych wejściowych]).
Biorąc pod uwagę ogólną implementację rekurencji, zmieniliśmy kod, który używa stosu wywołań, w kod, który używa innego stosu do przechowywania operacji oczekujących. Moglibyśmy zatem argumentować, że nadal jest rekurencyjny, gdy rozpatrywany jest na tak niskim poziomie.
Na tym poziomie faktycznie nie ma innych sposobów. Więc jeśli uważasz, że ta metoda jest rekurencyjna, to rzeczywiście są rzeczy, bez których nie możemy się obejść. Generalnie jednak nie oznaczamy takiego kodu rekurencyjnym. Termin rekurencja jest użyteczny, ponieważ obejmuje pewien zestaw podejść i umożliwia nam rozmowę o nich, a my nie używamy już żadnego z nich.
Oczywiście wszystko to zakłada, że masz wybór. Istnieją zarówno języki, które zabraniają wywołań rekurencyjnych, jak i języki, w których brakuje struktur zapętlania niezbędnych do iteracji.