web-dev-qa-db-ja.com

末尾再帰と前方再帰

誰かが私にこれらの2種類の再帰と例(特にOCaml)の違いを教えてもらえますか?

21
REALFREE

末尾再帰関数は、再帰呼び出しが関数の最後の呼び出しのみである関数です。非末尾再帰関数は、そうではない関数です。

後方再帰は、各再帰呼び出しでパラメーターの値が前のステップよりも小さい再帰です。前方再帰は、ステップごとに大きくなる再帰です。

これらは2つの直交する概念です。つまり、前方再帰は末尾再帰である場合とそうでない場合があり、同じことが後方再帰にも当てはまります。

たとえば、階乗関数は命令型言語で次のように記述されることがよくあります。

fac = 1
for i from 1 to n:
    fac := fac * i

階乗の一般的な再帰バージョンは逆方向にカウントします(つまり、パラメーターとしてn-1を使用してそれ自体を呼び出します)が、上記の命令型ソリューションを直接変換すると、上向きにカウントする再帰バージョンが思い浮かびます。次のようになります。

let fac n =
  let rec loop i =
    if i >= n
    then i
    else i * loop (i+1)
  in
    loop 1

これは順方向の再帰であり、ご覧のとおり、ヘルパー関数が必要なため、逆方向の再帰的なバリアントよりも少し面倒です。 loopの最後の呼び出しは乗算であり、再帰ではないため、これは末尾再帰ではありません。したがって、末尾再帰にするには、次のようにします。

let fac n =
  let rec loop acc i =
    if i >= n
    then acc
    else loop (i*acc) (i+1)
  in
    loop 1 1

再帰呼び出しはa)末尾呼び出しであり、b)より大きな値(i+1)で自分自身を呼び出すため、これは順方向再帰と末尾再帰の両方になります。

29
sepp2k

末尾再帰階乗関数の例を次に示します。

let fac n =
let rec f n a =
    match n with
    0 -> a
    | _ -> f (n-1) (n*a)
in
f n 1

これが非末尾再帰の対応物です:

let rec non_tail_fac n =
match n with
0 -> 1
| _ -> (non_tail_fac n-1) * n

末尾再帰関数は、アキュムレータaを使用して、前の呼び出しの結果の値を格納します。これにより、OCamlは末尾呼び出しの最適化を実行でき、スタックがオーバーフローしなくなります。通常、末尾再帰関数はアキュムレータ値を利用して、末尾呼び出しの最適化を実行できるようにします。

9
sashang

たとえば、再帰関数build_Wordは、char listを受け取り、それらを文字列に結合します。つまり、['f'; 'o'; 'o']から文字列"foo"に結合します。誘導プロセスは次のように視覚化できます。

build_Word ['f'; 'o'; 'o']
"f" ^ (build_Word ['o'; 'o'])
"f" ^ ("o" ^ (build_Word ['o'])    // base case! return "o" and fold back
"f" ^ ("o" ^ ("o"))
"f" ^ ("oo")
"foo"

それは通常の再帰でした。括弧の各ペアは、新しいスタックフレームまたは再帰呼び出しを表すことに注意してください。この問題の解決策(つまり、「f」、「fo」、または「foo」)は、再帰が終了する前に(基本ケースが満たされる場合)導出できません。その場合にのみ、最後のフレームは「ポップ」する前に最後の結果を前の結果に戻します。その逆も同様です。

理論的には、呼び出しごとに新しいスタックフレーム(または、必要に応じてスコープ)が作成され、フラグメント化されたソリューションが最初に返され、収集される「場所」が保持されます。これは stackoverflow につながる可能性があります(このリンクは再帰です)。

末尾呼び出しバージョンは次のようになります。

build_Word ['f'; 'o'; 'o'] ""
build_Word ['o'; 'o'], "f"
build_Word ['o'] ("f" ^ "o")
build_Word [] ("f" ^ "o" ^ "o")
"foo"

ここでは、累積された結果(多くの場合、accumulatorと呼ばれる変数に格納されます)が転送されます。最適化を使用すると、末尾呼び出しは前のスタックフレームを維持する必要がないため、新しいスタックフレームを作成する必要がありません。解決策は、「後方」ではなく「前方」で解決されています。

2つのバージョンのbuild_Word関数は次のとおりです。

非テール

let build_Word chars = 
  match chars with
  | [] -> None
  | [c] -> Some Char.to_string c
  | hd :: tl -> build_Word tl
;;

テール

let build_Word ?(acc = "") chars =
  match chars with
  | [] -> None
  | [c] -> Some Char.to_string c
  | hd::tl -> build_Word ~acc:(acc ^ Char.to_string hd) tl
;;

前方再帰は、@ sepp2kによって受け入れられた回答で十分に説明されています。

0
Pie 'Oh' Pah