Es kommt darauf an, wie streng Sie "Rekursion" definieren.
Wenn wir unbedingt verlangen, dass der Aufruf-Stack einbezogen wird (oder welcher Mechanismus zur Aufrechterhaltung des Programmstatus auch immer verwendet wird), können wir ihn jederzeit durch etwas ersetzen, das dies nicht tut. In der Tat haben Sprachen, die auf natürliche Weise zu einer starken Nutzung der Rekursion führen, in der Regel Compiler, die die Tail-Call-Optimierung stark nutzen. Was Sie also schreiben, ist rekursiv, was Sie ausführen, ist iterativ.
Betrachten wir jedoch einen Fall, in dem wir einen rekursiven Aufruf ausführen und das Ergebnis eines rekursiven Aufrufs für diesen rekursiven Aufruf verwenden.
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));
}
Es ist einfach, den ersten rekursiven Aufruf zu wiederholen:
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));
}
Wir können dann goto
die Velociraptoren und den Schatten von Dijkstra entfernen , um sie abzuwehren :
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;
}
Um die anderen rekursiven Aufrufe zu entfernen, müssen wir die Werte einiger Aufrufe in einem Stapel speichern:
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;
}
Wenn wir nun den Quellcode betrachten, haben wir unsere rekursive Methode zweifellos in eine iterative Methode umgewandelt.
In Anbetracht dessen, wozu dies kompiliert wurde, haben wir Code, der den Aufrufstapel verwendet, um eine Rekursion in Code zu implementieren, der dies nicht tut, umgewandelt (und dabei Code, der eine Stapelüberlauf-Ausnahme für selbst recht kleine Werte in Code auslöst, der nur wird) es dauert unglaublich lange, bis ich zurückkomme [siehe Wie kann ich verhindern, dass meine Ackerman-Funktion den Stapel überläuft? für einige weitere Optimierungen, die dafür sorgen, dass er tatsächlich für viele weitere mögliche Eingaben zurückkommt).
In Anbetracht der allgemeinen Implementierung der Rekursion haben wir Code, der den Aufrufstapel verwendet, in Code umgewandelt, der einen anderen Stapel verwendet, um ausstehende Vorgänge zu speichern. Wir könnten daher argumentieren, dass es immer noch rekursiv ist, wenn man es auf diesem niedrigen Niveau betrachtet.
Und auf dieser Ebene gibt es in der Tat keine anderen Möglichkeiten. Wenn Sie diese Methode als rekursiv betrachten, dann gibt es tatsächlich Dinge, die wir ohne sie nicht tun können. Im Allgemeinen wird ein solcher Code jedoch nicht als rekursiv bezeichnet. Der Begriff Rekursion ist nützlich, weil er eine Reihe von Ansätzen abdeckt und uns die Möglichkeit gibt, darüber zu sprechen, und wir verwenden keinen von ihnen mehr.
All dies setzt natürlich voraus, dass Sie die Wahl haben. Es gibt sowohl Sprachen, die rekursive Aufrufe verbieten, als auch Sprachen, denen die für die Iteration erforderlichen Schleifenstrukturen fehlen.