「再帰」をどの程度厳密に定義するかによります。
コールスタックを使用することを厳密に要求する場合(またはプログラムの状態を維持するためのメカニズムを使用する場合)、いつでもそれを使用しないものに置き換えることができます。実際、自然に再帰を頻繁に使用する言語には、末尾呼び出しの最適化を頻繁に使用するコンパイラが含まれる傾向があるため、記述は再帰的ですが、実行は反復的です。
しかし、再帰呼び出しを行い、その再帰呼び出しに再帰呼び出しの結果を使用する場合を考えてみましょう。
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;
}
さて、ソースコードを検討するとき、再帰メソッドを確実に反復メソッドに変えました。
これが何にコンパイルされているかを考慮して、呼び出しスタックを使用するコードを再帰を実装しないコードに変換しました(そして、そうすることで、非常に小さな値でもスタックオーバーフロー例外をスローするコードを単にコードに変換しました)復帰するのに非常に長い時間がかかります(さらに多くの可能な入力に対して実際に復帰する最適化については、アッカーマン関数がスタックからオーバーフローしないようにする方法を参照してください)。
再帰の一般的な実装方法を考慮して、呼び出しスタックを使用するコードを、異なるスタックを使用して保留中の操作を保持するコードに変換しました。したがって、その低レベルで検討すると、それはまだ再帰的であると主張できます。
そして、そのレベルでは、実際に他の方法はありません。したがって、そのメソッドが再帰的であると考えると、それなしではできないことは確かにあります。通常、このようなコードには再帰的なラベルを付けませんが。再帰という用語は、特定のアプローチのセットをカバーし、それらについて話す方法を提供し、そのうちの1つを使用しなくなったため、便利です。
もちろん、これはすべて選択肢があることを前提としています。再帰呼び出しを禁止する言語と、反復に必要なループ構造を持たない言語の両方があります。