末尾再帰とは何ですか?


52

再帰の一般的な概念を知っています。クイックソートアルゴリズムを勉強しているときに、テール再帰の概念に出会いました。MITの 18:30秒のクイックソートアルゴリズムのこのビデオでは、教授はこれが末尾再帰アルゴリズムであると述べています。末尾再帰が本当に何を意味するのかは私には明らかではありません。

誰かが適切な例で概念を説明できますか?

ここで SOコミュニティによって提供されたいくつかの答え。


末尾再帰という用語に遭遇した文脈について詳しく教えてください。リンク?引用?
A.シュルツ

@ A.Schulzコンテキストへのリンクを配置しました。
オタク

5
見て、「何で末尾再帰? StackOverflowの上で」
はVor

2
@ajmartin質問はStack Overflowの境界線ですが、Computer Scienceのトピックにしっかりと関連しているため、原則としてComputer Scienceはより良い答えを生成するはずです。ここでは発生していませんが、より良い回答を期待してここで再度質問しても構いません。オタク、SOで以前の質問に言及するべきだったので、人々はすでに言われたことを繰り返さないように。
ジル 'SO-悪であるのをやめる'

1
また、曖昧な部分が何なのか、以前の回答に満足していないのはなぜかを言う必要があります。

回答:


52

末尾再帰は再帰の特殊なケースであり、呼び出し元の関数は再帰呼び出しを行った後は計算を行いません。たとえば、関数

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;
}

再帰呼び出しが返された後に何らかの計算を行うためです。

末尾再帰は、一般的な再帰よりも効率的に実装できるため重要です。通常の再帰呼び出しを行う場合、戻りアドレスを呼び出しスタックにプッシュし、呼び出された関数にジャンプする必要があります。これは、再帰呼び出しの深さでサイズが線形の呼び出しスタックが必要であることを意味します。末尾再帰がある場合、再帰呼び出しから戻るとすぐに戻ることもわかっているので、再帰関数のチェーン全体をスキップして元の呼び出し元に直接戻ることができます。つまり、すべての再帰呼び出しに呼び出しスタックは必要なく、最終呼び出しを単純なジャンプとして実装でき、スペースを節約できます。


2
「つまり、すべての再帰呼び出しに呼び出しスタックが必要ないことを意味します」と書きました。コールスタックは常にそこにありますが、リターンアドレスをコールスタックに書き込む必要はありません。
オタク

2
計算のモデルにある程度依存します:)しかし、はい、実際のコンピューターでは呼び出しスタックはまだ存在しているので、使用していません。
マットルイス

forループの最終呼び出しである場合はどうなるでしょう。したがって、上記のすべての計算を行いますが、その一部はforループで行われますdef recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
-thed0ctor

13

簡単に言うと、末尾再帰は、コンパイラが再帰呼び出しを「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;ます。ただし、これは、再帰呼び出しによって返された値が直接返される場合にのみ実行できます。

場合は、すべての関数で再帰呼び出しは次のように置き換えることができ、それは末尾再帰関数です。


13

私の答えは、「コンピュータプログラムの構造と解釈」という本に記載されている説明に基づいています。この本をコンピュータ科学者に強くお勧めします。

アプローチA:線形再帰プロセス

(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

アプローチB:線形反復プロセス

(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には、インタープリターが保持する隠された情報がより多くあります。これは基本的に遅延操作のチェーンです。


5

末尾再帰とは、再帰呼び出しが関数の最後の命令である再帰の形式です(末尾部分の由来です)。さらに、再帰呼び出しは、以前の値(関数のパラメーター以外の参照)を格納するメモリセルへの参照で構成しないでください。この方法では、以前の値を気にする必要はなく、すべての再帰呼び出しに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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.