末尾再帰の仕組みと、通常の再帰との違いをほとんど理解しています。 I only理由がわからないdoes n'tスタックに戻りアドレスを記憶する必要がある.
// tail recursion
int fac_times (int n, int acc) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
int factorial (int n) {
return fac_times (n, 1);
}
// normal recursion
int factorial (int n) {
if (n == 0) return 1;
else return n * factorial(n - 1);
}
末尾再帰関数で関数自体を呼び出した後は何もする必要はありませんが、私には意味がありません。
コンパイラはこれを単純に変換できます
int fac_times (int n, int acc) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
このようなものに:
int fac_times (int n, int acc) {
label:
if (n == 0) return acc;
acc *= n--;
goto label;
}
なぜ「戻りアドレスを覚えるのにスタックを必要としないのか」と尋ねます。
これを好転させたい。 It doesスタックを使用して戻りアドレスを記憶します。秘Theは、末尾再帰が発生する関数がスタック上に独自の戻りアドレスを持ち、呼び出された関数にジャンプすると、これを独自の戻りアドレスとして扱うことです。
具体的には、テールコールの最適化なし:
f: ...
CALL g
RET
g:
...
RET
この場合、g
が呼び出されると、スタックは次のようになります。
SP -> Return address of "g"
Return address of "f"
一方、末尾呼び出しの最適化では:
f: ...
JUMP g
g:
...
RET
この場合、g
が呼び出されると、スタックは次のようになります。
SP -> Return address of "f"
明らかに、g
が戻ると、f
が呼び出された場所に戻ります。
[〜#〜] edit [〜#〜]:上記の例では、ある関数が別の関数を呼び出す場合を使用しています。関数がそれ自体を呼び出すときのメカニズムは同一です。
通常の再帰関数の戻り値は、2種類の値で構成されています。
あなたの例を見てみましょう:
int factorial (int n) {
if (n == 0) return 1;
else return n * factorial(n - 1);
}
フレームf(5)は、独自の計算結果(5)とf(4)の値を「格納」します。factorial(5)を呼び出す場合、スタック呼び出しが崩壊し始め、私は持っています:
[Stack_f(5): return 5 * [Stack_f(4): 4 * [Stack_f(3): 3 * ... [1[1]]
各スタックには、前述の値に加えて、関数のスコープ全体が格納されることに注意してください。したがって、再帰関数fのメモリ使用量はO(x)です。ここで、xは、実行する必要がある再帰呼び出しの数です。したがって、factorial(1)またはfactorial(2)を計算するためにRAMの1kbが必要な場合、factorial(100)を計算するために〜100kが必要です。
末尾再帰では、パラメーターを使用して、各再帰フレームでの部分計算の結果を次のフレームに渡します。階乗の例、Tail Recursiveを見てみましょう。
int階乗(int n){intヘルパー(int num、int累積){if num == 0 return return累積else return returnerer(num-1、累計* num)} return helper(n、1)
}
Factorial(4)のフレームを見てみましょう:
[Stack f(4, 5): Stack f(3, 20): [Stack f(2,60): [Stack f(1, 120): 120]]]]
違いがわかりますか? 「通常の」再帰呼び出しでは、戻り関数は再帰的に最終値を構成します。末尾再帰では、基本ケース(最後に評価されたもの)のみを参照します。 accumulator古い値を追跡する引数を呼び出します。
通常の再帰関数は次のようになります。
type regular(n)
base_case
computation
return (result of computation) combined with (regular(n towards base case))
Tail再帰で変換するには:
見て:
type tail(n):
type helper(n, accumulator):
if n == base case
return accumulator
computation
accumulator = computation combined with accumulator
return helper(n towards base case, accumulator)
helper(n, base case)
違いを見ます?
テールコールスタックの非境界ケースには状態が格納されていないため、これらはそれほど重要ではありません。一部の言語/通訳者は、古いスタックを新しいスタックに置き換えます。そのため、呼び出しの数を制限するスタックフレームがないため、これらのケースではTail Callsはforループのように動作しますです。
最適化するかどうかはコンパイラ次第です。
特にアキュムレータが使用されている場合、テール再帰は通常コンパイラによってループに変換できます。
// tail recursion
int fac_times (int n, int acc = 1) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
のようなものにコンパイルします
// accumulator
int fac_times (int n) {
int acc = 1;
while (n > 0) {
acc *= n;
n -= 1;
}
return acc;
}
再帰関数がどのように機能するかを示す簡単な例を次に示します。
long f (long n)
{
if (n == 0) // have we reached the bottom of the ocean ?
return 0;
// code executed in the descendence
return f(n-1) + 1; // recurrence
// code executed in the ascendence
}
末尾再帰は単純な再帰関数であり、関数の最後で再帰が行われるため、昇順ではコードが実行されないため、高レベルプログラミング言語のほとんどのコンパイラーが 末尾再帰最適化 、 Tail recursion modulo として知られるより複雑な最適化もあります
再帰関数は、単独で呼び出す
これにより、プログラマはコードの最小量を使用して効率的なプログラムを作成できます。
欠点は、無限ループの原因および適切に記述されていないの場合、他の予期しない結果になる可能性があることです。
単純再帰関数と末尾再帰関数の両方を説明します
単純な再帰関数を書くために
与えられた例から:
_public static int fact(int n){
if(n <=1)
return 1;
else
return n * fact(n-1);
}
_
上記の例から
_if(n <=1)
return 1;
_
ループを終了する決定要因です
_else
return n * fact(n-1);
_
実際に行われる処理ですか
わかりやすくするために、タスクを1つずつ壊してみましょう。
fact(4)
を実行すると、内部で何が起こるか見てみましょう
_public static int fact(4){
if(4 <=1)
return 1;
else
return 4 * fact(4-1);
}
_
If
ループは失敗するため、else
ループに進み、4 * fact(3)
を返します。
スタックメモリには、4 * fact(3)
があります
n = 3の置換
_public static int fact(3){
if(3 <=1)
return 1;
else
return 3 * fact(3-1);
}
_
If
ループが失敗するため、else
ループに進みます
したがって、3 * fact(2)
を返します
`` `4 * fact(3)` `を呼び出したことを思い出してください
fact(3) = 3 * fact(2)
の出力
これまでのところ、スタックには4 * fact(3) = 4 * 3 * fact(2)
があります
スタックメモリには、4 * 3 * fact(2)
があります
n = 2の置換
_public static int fact(2){
if(2 <=1)
return 1;
else
return 2 * fact(2-1);
}
_
If
ループが失敗するため、else
ループに進みます
したがって、2 * fact(1)
を返します
4 * 3 * fact(2)
を呼び出したことを思い出してください
fact(2) = 2 * fact(1)
の出力
これまでのところ、スタックには4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
があります
スタックメモリには、4 * 3 * 2 * fact(1)
があります
n = 1を代入
_public static int fact(1){
if(1 <=1)
return 1;
else
return 1 * fact(1-1);
}
_
If
ループはtrue
したがって、_1
_を返します
4 * 3 * 2 * fact(1)
を呼び出したことを思い出してください
fact(1) = 1
の出力
これまでのところ、スタックには4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
があります
最後に、fact(4)= 4 * 3 * 2 * 1 = 24の結果
Tail Recursionは
_public static int fact(x, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(x-1, running_total*x);
}
}
_
_public static int fact(4, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(4-1, running_total*4);
}
}
_
If
ループは失敗するため、else
ループに進み、fact(3, 4)
を返します。
スタックメモリには、fact(3, 4)
があります
n = 3の置換
_public static int fact(3, running_total=4) {
if (x==1) {
return running_total;
} else {
return fact(3-1, 4*3);
}
}
_
If
ループが失敗するため、else
ループに進みます
したがって、fact(2, 12)
を返します
スタックメモリには、fact(2, 12)
があります
n = 2の置換
_public static int fact(2, running_total=12) {
if (x==1) {
return running_total;
} else {
return fact(2-1, 12*2);
}
}
_
If
ループが失敗するため、else
ループに進みます
したがって、fact(1, 24)
を返します
スタックメモリには、fact(1, 24)
があります
n = 1を代入
_public static int fact(1, running_total=24) {
if (x==1) {
return running_total;
} else {
return fact(1-1, 24*1);
}
}
_
If
ループはtrue
したがって、_running_total
_を返します
_running_total = 24
_の出力
最後に、fact(4,1)= 24の結果
コンパイラーは、末尾再帰を理解するのに十分インテリジェントです。再帰呼び出しから戻る間、保留中の操作はなく、再帰呼び出しは最後のステートメントであり、末尾再帰のカテゴリーに分類されます。コンパイラは基本的に末尾再帰の最適化を実行し、スタックの実装を削除します。コード以下を検討してください。
void tail(int i) {
if(i<=0) return;
else {
system.out.print(i+"");
tail(i-1);
}
}
最適化を実行した後、上記のコードは以下のコードに変換されます。
void tail(int i) {
blockToJump:{
if(i<=0) return;
else {
system.out.print(i+"");
i=i-1;
continue blockToJump; //jump to the bolckToJump
}
}
}
これは、コンパイラがテール再帰最適化を行う方法です。
再帰は内部実装に関連するものであるため、私の答えは推測に過ぎません。
末尾再帰では、再帰関数は同じ関数の最後に呼び出されます。おそらくコンパイラは以下の方法で最適化できます:
ご覧のとおり、同じ関数の次の反復の前に元の関数を巻き上げているため、実際にはスタックを「使用」していません。
ただし、関数内で呼び出されるデストラクタがある場合、この最適化は適用されない可能性があります。