再帰の一般的な概念を知っています。クイックソートアルゴリズムを勉強しているときに、テール再帰の概念に出会いました。MITの 18:30秒のクイックソートアルゴリズムのこのビデオでは、教授はこれが末尾再帰アルゴリズムであると述べています。末尾再帰が本当に何を意味するのかは私には明らかではありません。
誰かが適切な例で概念を説明できますか?
ここで SOコミュニティによって提供されたいくつかの答え。
再帰の一般的な概念を知っています。クイックソートアルゴリズムを勉強しているときに、テール再帰の概念に出会いました。MITの 18:30秒のクイックソートアルゴリズムのこのビデオでは、教授はこれが末尾再帰アルゴリズムであると述べています。末尾再帰が本当に何を意味するのかは私には明らかではありません。
誰かが適切な例で概念を説明できますか?
ここで SOコミュニティによって提供されたいくつかの答え。
回答:
末尾再帰は再帰の特殊なケースであり、呼び出し元の関数は再帰呼び出しを行った後は計算を行いません。たとえば、関数
int f(int x、int y){ if(y == 0){ return x; } return f(x * y、y-1); }
(最後の命令は再帰呼び出しであるため)は末尾再帰であるのに対し、この関数は末尾再帰ではありません。
int g(int x){ if(x == 1){ return 1; } int y = g(x-1); return x * y; }
再帰呼び出しが返された後に何らかの計算を行うためです。
末尾再帰は、一般的な再帰よりも効率的に実装できるため重要です。通常の再帰呼び出しを行う場合、戻りアドレスを呼び出しスタックにプッシュし、呼び出された関数にジャンプする必要があります。これは、再帰呼び出しの深さでサイズが線形の呼び出しスタックが必要であることを意味します。末尾再帰がある場合、再帰呼び出しから戻るとすぐに戻ることもわかっているので、再帰関数のチェーン全体をスキップして元の呼び出し元に直接戻ることができます。つまり、すべての再帰呼び出しに呼び出しスタックは必要なく、最終呼び出しを単純なジャンプとして実装でき、スペースを節約できます。
def recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
簡単に言うと、末尾再帰は、コンパイラが再帰呼び出しを「goto」コマンドに置き換えることができる再帰であるため、コンパイルされたバージョンではスタックの深さを増やす必要はありません。
末尾再帰関数を設計するには、追加のパラメーターを使用してヘルパー関数を作成する必要がある場合があります。
たとえば、これは末尾再帰関数ではありません。
int factorial(int x) {
if (x > 0) {
return x * factorial(x - 1);
}
return 1;
}
ただし、これは末尾再帰関数です。
int factorial(int x) {
return tailfactorial(x, 1);
}
int tailfactorial(int x, int multiplier) {
if (x > 0) {
return tailfactorial(x - 1, x * multiplier);
}
return multiplier;
}
コンパイラは、次のようなもの(擬似コード)を使用して、再帰関数を非再帰関数に書き換えることができるためです。
int tailfactorial(int x, int multiplier) {
start:
if (x > 0) {
multiplier = x * multiplier;
x--;
goto start;
}
return multiplier;
}
コンパイラのルールは非常に単純です。「return thisfunction(newparameters);
」が見つかったら、「」に置き換えparameters = newparameters; goto start;
ます。ただし、これは、再帰呼び出しによって返された値が直接返される場合にのみ実行できます。
場合は、すべての関数で再帰呼び出しは次のように置き換えることができ、それは末尾再帰関数です。
私の答えは、「コンピュータプログラムの構造と解釈」という本に記載されている説明に基づいています。この本をコンピュータ科学者に強くお勧めします。
(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1)))))
アプローチAのプロセスの形状は次のようになります。
(factorial 5)
(* 5 (factorial 4))
(* 5 (* 4 (factorial 3)))
(* 5 (* 4 (* 3 (factorial 2))))
(* 5 (* 4 (* 3 (* 2 (factorial 1)))))
(* 5 (* 4 (* 3 (* 2 (* 1)))))
(* 5 (* 4 (* 3 (* 2))))
(* 5 (* 4 (* 6)))
(* 5 (* 24))
120
(define (factorial n)
(fact-iter 1 1 n))
(define (fact-iter product counter max-count)
(if (> counter max-count)
product
(fact-iter (* counter product)
(+ counter 1)
max-count)))
アプローチBのプロセスの形状は次のようになります。
(factorial 5)
(fact-iter 1 1 5)
(fact-iter 1 2 5)
(fact-iter 2 3 5)
(fact-iter 6 4 5)
(fact-iter 24 5 5)
(fact-iter 120 6 5)
120
線形反復プロセス(アプローチB)は、プロセスが再帰的プロシージャであっても、一定のスペースで実行されます。また、このアプローチでは、設定変数が任意の時点でのプロセスの状態を定義することにも注意する必要があります。{product, counter, max-count}
。これは、末尾再帰によりコンパイラの最適化を可能にする手法でもあります。
アプローチAには、インタープリターが保持する隠された情報がより多くあります。これは基本的に遅延操作のチェーンです。
末尾再帰とは、再帰呼び出しが関数の最後の命令である再帰の形式です(末尾部分の由来です)。さらに、再帰呼び出しは、以前の値(関数のパラメーター以外の参照)を格納するメモリセルへの参照で構成しないでください。この方法では、以前の値を気にする必要はなく、すべての再帰呼び出しに1つのスタックフレームで十分です。末尾再帰は、再帰アルゴリズムを最適化する1つの方法です。他の利点/最適化は、末尾再帰アルゴリズムを、再帰ではなく反復を使用する同等のアルゴリズムに変換する簡単な方法があることです。はい、クイックソートのアルゴリズムは確かに末尾再帰です。
QUICKSORT(A, p, r)
if(p < r)
then
q = PARTITION(A, p, r)
QUICKSORT(A, p, q–1)
QUICKSORT(A, q+1, r)
反復バージョンは次のとおりです。
QUICKSORT(A)
p = 0, r = len(A) - 1
while(p < r)
q = PARTITION(A, p, r)
r = q - 1
p = 0, r = len(A) - 1
while(p < r)
q = PARTITION(A, p, r)
p = q + 1