web-dev-qa-db-ja.com

再帰関数の仕組みを理解する

タイトルが説明しているように、私は非常に基本的なプログラミングの質問があり、まだ理解できていません。 (非常に巧妙な)「再帰を理解するためには、まず再帰を理解する必要があります」すべてを除外します。さまざまなオンラインスレッドからの返信はまだ受け取っていません。

私たちが知らないことを知らないことに直面すると、間違った質問をしたり、間違った質問をしたりする傾向があることを理解しています。私は私の質問が「考えている」ことを共有します再帰電球をオンにするのに役立つ知識が少しあります!

関数は次のとおりです(構文はSwiftで記述されています)。

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a: a + 1, b: b)
    }
}

引数として2と5を使用します。

println(sumInts(a: 2, b: 5))

明らかに、答えは14です。しかし、その価値がどのように達成されるかについては明確ではありません。

これらは私の2つのハングアップです。

  1. 関数は、条件が満たされるまで再帰的に呼び出されます。その条件はa> bです。この条件が満たされたら、0を返します。一見、戻り値が0であると予想されますが、これは明らかに正しくありません。

  2. 各反復で「a」の値を出力すると、期待どおりの値が得られます:2、3、4、5(その時点で5 + 1> bで、最初の条件:a> bを満たします) 14の値がどのように達成されるかを確認してください。

私が最初に考えたのは、次のようなことが魔法のように起こっているということです。

var answer = a;
answer += a+1 until a > b;
return answer;   

だから魔法を除外して、私は何かを得ていないだけです。暗黙的にだけでなく、何が起こっているのかを理解したいと思います。

誰かがこの種の機能中に技術的に何が起こるか、なぜ結果が0でなく、最終的にa + sumInts(a: a + 1, b: b) = 14であるかを親切に説明できれば、私はあなたの借金に永遠にいるでしょう。

113
Jason Elwood

I 思考混乱は、「同じ機能」が何度も呼び出されていると考えることに起因しています。 「同じ関数の多くのコピーが呼び出されている」と考えると、より明確になる可能性があります。

関数のコピーは1つだけが0を返し、最初のコピーではありません(最後のコピーです)。したがって、最初の呼び出しの結果は0ではありません。

2番目の混乱については、再帰を英語で説明する方が簡単だと思います。この行を読んでください:

return a + sumInts(a + 1, b: b)

「「a」の値を返します(「a」のコピーの値である関数の別のコピーの戻り値プラス(「a」のコピーの値である関数の別のコピーの戻り値a 'plus(... "。関数の各コピーが、a> bの条件が満たされるまで、1ずつ増加してそれ自体の新しいコピーを生成します。

A> b条件がtrueになるまでに、実行中の関数のコピーの(潜在的に任意の)長いスタックがあり、すべて次のコピーの結果を待ってそれらが何であるかを見つけます。 「a」に追加する必要があります。

(編集:また、私が言及した関数のコピーのスタックは、実際のメモリを占有する実際のものであり、プログラムが大きくなりすぎるとプログラムがクラッシュします。コンパイラはそれを最適化することができますただし、スタックスペースを使い果たすことは、多くの言語の再帰関数の重大で不幸な制限です)

105
Catfish_Man

1.関数は条件が満たされるまで再帰的に呼び出されます。その条件はa > bです。この条件が満たされたら、0を返します。一見、戻り値が0であると予想されますが、これは明らかに正しくありません。

コンピュータコンピューティングsumInts(2,5)は、次のことができた場合にどのように考えるかを示します。

I want to compute sumInts(2, 5)
for this, I need to compute sumInts(3, 5)
and add 2 to the result.
  I want to compute sumInts(3, 5)
  for this, I need to compute sumInts(4, 5)
  and add 3 to the result.
    I want to compute sumInts(4, 5)
    for this, I need to compute sumInts(5, 5)
    and add 4 to the result.
      I want to compute sumInts(5, 5)
      for this, I need to compute sumInts(6, 5)
      and add 5 to the result.
        I want to compute sumInts(6, 5)
        since 6 > 5, this is zero.
      The computation yielded 0, therefore I shall return 5 = 5 + 0.
    The computation yielded 5, therefore I shall return 9 = 4 + 5.
  The computation yielded 9, therefore I shall return 12 = 3 + 9.
The computation yielded 12, therefore I shall return 14 = 2 + 12.

ご覧のように、関数sumIntsの呼び出しは実際には0を返しますが、コンピューターはまだ5をその0に追加し、次に4を追加し、3、2を追加する必要があるため、これは最終値ではありませんコンピューターの思考に関する最後の4つの文。再帰では、コンピューターは再帰呼び出しを計算するだけでなく、再帰呼び出しによって返された値をどう処理するかを覚えておく必要があることに注意してください。 スタックと呼ばれるコンピューターのメモリの特別な領域があり、この種の情報が保存され、このスペースは制限され、再帰的すぎる関数はスタックを使い果たす可能性があります:これはスタックオーバーフローです最も愛されているウェブサイトに名前を付けています。

あなたの声明は、再帰呼び出しを行ったときにコンピューターが何をしていたかをコンピューターが忘れているという暗黙の仮定を立てているように見えますが、そうではないので、結論は観察と一致しません。

2.各反復で「a」の値を印刷すると、期待される値が得られます:2、3、4、5(その時点で5 + 1> bで、最初の条件を満たす:a> b) 14の値がどのように達成されるかわかりません。

これは、戻り値がaそのものではなく、aの値と再帰呼び出しによって返された値の合計であるためです。

再帰を理解するには、問題を別の方法で考える必要があります。全体として意味のある大きな論理的な手順の代わりに、代わりに大きな問題を取り上げて小さな問題に分割して解決し、サブ問題の答えが得られたらサブ問題の結果を組み合わせてより大きな問題の解決策。あなたとあなたの友人が巨大なバケツの中のビー玉の数を数える必要があると考えてください。あなたはそれぞれ小さなバケツを取り、それらを個別に数えて行き、終わったら合計を合計します。さて、あなたがそれぞれの友達を見つけてバケットをさらに分割したら、あなたはこれらの他の友達がそれらの合計を把握し、それを各自に持ち帰り、合計します。等々。特別な場合は、数える大理石が1つだけの場合は、それを返して1と言うだけです。上の他の人に追加を完了させます。

関数がそれ自体を再帰的に呼び出すたびに、問題のサブセットを含む新しいコンテキストが作成され、その部分が解決されると、前の反復が完了するように返されることを覚えておく必要があります。

手順を示します。

sumInts(a: 2, b: 5) will return: 2 + sumInts(a: 3, b: 5)
sumInts(a: 3, b: 5) will return: 3 + sumInts(a: 4, b: 5)
sumInts(a: 4, b: 5) will return: 4 + sumInts(a: 5, b: 5)
sumInts(a: 5, b: 5) will return: 5 + sumInts(a: 6, b: 5)
sumInts(a: 6, b: 5) will return: 0

sumInts(a:6、b:5)が実行されると、結果を計算できるため、取得した結果でチェーンを遡ることができます。

 sumInts(a: 6, b: 5) = 0
 sumInts(a: 5, b: 5) = 5 + 0 = 5
 sumInts(a: 4, b: 5) = 4 + 5 = 9
 sumInts(a: 3, b: 5) = 3 + 9 = 12
 sumInts(a: 2, b: 5) = 2 + 12 = 14.

再帰の構造を表す別の方法:

 sumInts(a: 2, b: 5) = 2 + sumInts(a: 3, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + sumInts(a: 4, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + sumInts(a: 5, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + sumInts(a: 6, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + 0
 sumInts(a: 2, b: 5) = 14 
47
Rob

再帰は理解するのが難しいトピックであり、ここでは完全に正義を実行できるとは思わない。代わりに、ここにある特定のコードに焦点を当て、ソリューションが機能する理由の直観と、コードが結果を計算する方法のメカニズムの両方を説明しようとします。

ここで指定したコードは、次の問題を解決します。aからbまでのすべての整数の合計を知りたい。あなたの例では、2から5までの数字の合計が必要です。

2 + 3 + 4 + 5

問題を再帰的に解決しようとする場合、最初のステップの1つは、問題を同じ構造を持つ小さな問題に分割する方法を見つけることです。したがって、2から5までの数を合計するとします。これを単純化する1つの方法は、上記の合計を次のように書き換えることができることに注意することです。

2 +(3 + 4 + 5)

ここで、(3 + 4 + 5)はたまたま3〜5のすべての整数の合計です。つまり、2〜5のすべての整数の合計を知りたい場合は、3〜5のすべての整数の合計を計算してから、2を加算します。

では、3から5までのすべての整数の合計をどのように計算しますか?まあ、その合計は

3 + 4 + 5

代わりに考えることができます

3 +(4 + 5)

ここで、(4 + 5)は4から5までのすべての整数の合計です。したがって、3から5までのすべての数値の合計を計算する場合は、4から5までのすべての整数の合計を計算してから3を加算します。

ここにパターンがあります! aとbの間の整数の合計を計算する場合は、次のことができます。まず、a + 1からbまでの整数の合計を計算します。次に、その合計にaを追加します。 「a + 1とbの間の整数の合計を計算する」は、私たちが既に解決しようとしている問題とほぼ同じ種類ですが、パラメーターがわずかに異なることがわかります。 aからbまでを計算するのではなく、a + 1からbまでを計算します。それが再帰的なステップです-大きな問題(「aからbまでの合計」)を解決するために、問題をそれ自体の小さなバージョン(「a + 1からbまでの合計」)に減らします。

上記のコードを見ると、次のステップがあることがわかります。

return a + sumInts(a + 1, b: b)

このコードは、上記のロジックを単に翻訳したものです。aからbまでの合計を計算する場合は、a + 1からbまでの合計(sumIntsの再帰呼び出し)から始め、aを追加します。

もちろん、このアプローチ自体は実際には機能しません。たとえば、5以上5以下のすべての整数の合計をどのように計算しますか?さて、現在のロジックを使用して、6から5までのすべての整数の合計を計算し、5を加算します。6から5までのすべての整数の合計をどのように計算しますか?さて、現在のロジックを使用して、7から5までのすべての整数の合計を計算し、6を加算します。ここで問題に気付くでしょう。

再帰的な問題解決では、問題の単純化をやめるのではなく、直接解決する方法が必要です。通常、答えをすぐに決定できる単純なケースを見つけて、単純なケースが発生したときに直接解決するようにソリューションを構築します。これは通常、ベースケースまたは再帰ベースと呼ばれます。

この特定の問題の基本的なケースは何ですか? aからbまでの整数を合計すると、aがbよりも大きい場合、答えは0になります。範囲内に数字はありません。したがって、ソリューションを次のように構成します。

  1. A> bの場合、答えは0です。
  2. それ以外の場合(a≤b)、次のように答えを取得します。
    1. A + 1とbの間の整数の合計を計算します。
    2. を追加して回答を取得します。

次に、この擬似コードを実際のコードと比較します。

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b: b)
    }
}

擬似コードで概説されたソリューションとこの実際のコードの間には、ほぼ正確に1対1のマップがあることに注意してください。最初のステップは基本ケースです-数値の空の範囲の合計を求めた場合、0が得られます。それ以外の場合、a + 1とbの間の合計を計算し、aを追加します。

これまでのところ、コードの背後にある高レベルのアイデアを示しました。しかし、他に2つの非常に良い質問がありました。まず、関数がa> bの場合に0を返すように指示しているのに、なぜこれが常に0を返さないのですか?第二に、14は実際にはどこから来たのですか?これらを順番に見てみましょう。

非常に単純なケースを試してみましょう。 sumInts(6, 5)を呼び出すとどうなりますか?この場合、コードをトレースすると、関数が0を返すだけであることがわかります。これは正しいことです。範囲内に数字がありません。今、もっと難しいことを試してください。 sumInts(5, 5)を呼び出すとどうなりますか?さて、ここで何が起こるかです:

  1. sumInts(5, 5)を呼び出します。 elseブランチに分類され、 `a + sumInts(6、5)の値を返します。
  2. sumInts(5, 5)sumInts(6, 5)が何であるかを判断するには、実行中の作業を一時停止し、sumInts(6, 5)を呼び出す必要があります。
  3. sumInts(6, 5)が呼び出されます。 ifブランチに入り、0を返します。ただし、sumIntsのこのインスタンスはsumInts(5, 5)によって呼び出されたため、戻り値はトップレベルの呼び出し元ではなく、sumInts(5, 5)に返されます。
  4. sumInts(5, 5)は、5を取得するために5 + sumInts(6, 5)を計算できるようになりました。次に、それをトップレベルの呼び出し元に返します。

ここで値5がどのように形成されたかに注目してください。 sumIntsへの1つのアクティブな呼び出しから始めました。それは別の再帰呼び出しを開始し、その呼び出しによって返された値は情報をsumInts(5, 5)に送り返しました。 sumInts(5, 5)への呼び出しは、次にいくつかの計算を行い、呼び出し元に値を返しました。

sumInts(4, 5)でこれを試してみると、次のようになります。

  • sumInts(4, 5)4 + sumInts(5, 5)を返そうとします。そのために、sumInts(5, 5)。を呼び出します。
    • sumInts(5, 5)5 + sumInts(6, 5)を返そうとします。そのために、sumInts(6, 5)を呼び出します。
    • sumInts(6, 5)は、sumInts(5, 5).</li> <li>sumInts(5、5)now has a value forsumInts(6、5), namely 0. It then returns5 + 0 = 5`に0を返します。
  • sumInts(4, 5)にはsumInts(5, 5)の値、つまり5があります。その後、4 + 5 = 9を返します。

つまり、返される値は、値を1つずつ合計することによって形成されます。そのたびに、sumIntsへの特定の再帰呼び出しによって返される値を1つ取り、aの現在の値を加算します。再帰が底を打つと、最も深い呼び出しは0を返します。ただし、その値は再帰呼び出しチェーンをすぐには抜けません。代わりに、値を1つ上の層の再帰呼び出しに返します。そのようにして、各再帰呼び出しは、もう1つだけ数字を追加し、チェーンの上位にそれを返し、全体の合計で頂点に達します。練習として、sumInts(2, 5)についてこれをトレースしてみてください。

お役に立てれば!

40
templatetypedef

ここまでにいくつかの良い答えがありましたが、別のタックが必要なもう1つを追加します。

まず、単純な再帰アルゴリズムに関する多くの記事を書いています。見る

http://ericlippert.com/tag/recursion/

http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

それらは最新のものであるため、下から始めてください。

第二に、これまでのところ、すべての答えは、関数のアクティベーションを考慮することにより、再帰的なセマンティクスを説明しています。それぞれ、各呼び出しが新しいactivationを行い、再帰呼び出しがこのアクティベーションのコンテキストで実行されること。それはそれを考えるのに良い方法ですが、別の同等の方法があります:smart text seach-and-replace

関数をもう少しコンパクトな形式に書き換えてみましょう。これを特定の言語であると考えないでください。

s = (a, b) => a > b ? 0 : a + s(a + 1, b)

それが理にかなっていることを願っています。条件演算子に慣れていない場合は、condition ? consequence : alternativeという形式であり、その意味は明確になります。

s(2,5)を評価したいので、呼び出しをテキストで関数本体に置き換えてから、a2に、b5に置き換えます。

s(2, 5) 
---> 2 > 5 ? 0 : 2 + s(2 + 1, 5)

次に、条件を評価します。テキストで2 > 5falseに置き換えます。

---> false ? 0 : 2 + s(2 + 1, 5)

ここで、すべての偽の条件を代替物で置き換え、すべての真の条件を結果で置き換えます。偽の条件のみを持っているので、textuallyその式を代替で置き換えます:

---> 2 + s(2 + 1, 5)

ここで、これらの+記号をすべて入力する手間を省くために、定数演算をその値にテキストで置き換えます。 (これはちょっとしたカンニングですが、すべての括弧を追跡する必要はありません!)

---> 2 + s(3, 5)

今度は、呼び出しの本体、aの場合は3、bの場合は5で検索と置換を行います。呼び出しの置換を括弧で囲みます。

---> 2 + (3 > 5 ? 0 : 3 + s(3 + 1, 5))

そして今、私たちは同じテキスト置換手順を続けています:

---> 2 + (false ? 0 : 3 + s(3 + 1, 5))  
---> 2 + (3 + s(3 + 1, 5))                
---> 2 + (3 + s(4, 5))                     
---> 2 + (3 + (4 > 5 ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (false ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(5, 5)))
---> 2 + (3 + (4 + (5 > 5 ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (false ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(6, 5))))
---> 2 + (3 + (4 + (5 + (6 > 5 ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + (true ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + 0)))
---> 2 + (3 + (4 + 5))
---> 2 + (3 + 9)
---> 2 + 12
---> 14

ここで行ったのは、単純なテキスト置換だけでした。本当に必要になるまで、「2 + 1」の代わりに「3」などを使用すべきではありませんでしたが、教育的には読みにくくなりました。

関数のアクティブ化とは、関数呼び出しを呼び出しの本文に置き換え、仮パラメーターを対応する引数に置き換えることです。かっこをインテリジェントに導入することに注意する必要がありますが、それ以外は単なるテキストの置換です。

もちろん、ほとんどの言語は実際にはテキストの置換として実装しませんが、論理的にそれはそれです。

では、無制限の再帰とは何ですか?テキストの置換が止まらない再帰!最終的に置換するsがなくなったステップに到達し、算術のルールを適用できることに注意してください。

22
Eric Lippert

私が通常再帰関数がどのように機能するかを理解する方法は、基本ケースを見て後方に作業することです。この関数に適用される手法を次に示します。

まず基本ケース:

sumInts(6, 5) = 0

次に、 コールスタック のすぐ上のコール

sumInts(5, 5) == 5 + sumInts(6, 5)
sumInts(5, 5) == 5 + 0
sumInts(5, 5) == 5

次に、呼び出しスタックでそのすぐ上の呼び出し:

sumInts(4, 5) == 4 + sumInts(5, 5)
sumInts(4, 5) == 4 + 5
sumInts(4, 5) == 9

等々:

sumInts(3, 5) == 3 + sumInts(4, 5)
sumInts(3, 5) == 3 + 9
sumInts(3, 5) == 12

等々:

sumInts(2, 5) == 2 + sumInts(3, 5)
sumInts(4, 5) == 2 + 12
sumInts(4, 5) == 14

関数の元の呼び出しに到達したことに注意してくださいsumInts(2, 5) == 14

これらの呼び出しが実行される順序:

sumInts(2, 5)
sumInts(3, 5)
sumInts(4, 5)
sumInts(5, 5)
sumInts(6, 5)

これらの呼び出しが戻る順序:

sumInts(6, 5)
sumInts(5, 5)
sumInts(4, 5)
sumInts(3, 5)
sumInts(2, 5)

returnの順に呼び出しをトレースすることで、関数がどのように動作するかについて結論に達したことに注意してください。

11
OregonTrail

再帰。コンピューターサイエンスでは、再帰は有限オートマトンのトピックで詳細に説明されています。

最も単純な形式では、自己参照です。たとえば、「私の車は車です」と言うことは、再帰的なステートメントです。問題は、ステートメントが無限の再帰であり、終了しないことです。 「車」のステートメントの定義では、「車」であるため、置き換えられる可能性があります。ただし、代替の場合は「私の車は車です」になるため、終わりはありません。

これは、「私の車はベントレーです。私の車は青です」という文言の場合は異なる可能性があります。その場合、車の2番目の状況での置換は「ベントレー」となり、「私のベントレーは青」になります。これらのタイプの置換は、コンピューターサイエンスで Context-Free Grammerrs を使用して数学的に説明されています。

実際の置換は生産ルールです。ステートメントがSで表され、carが「ベントレー」になる変数である場合、このステートメントは再帰的に再構築できます。

S -> "my"S | " "S | CS | "is"S | "blue"S | ε
C -> "bentley"

|は選択肢があることを意味するため、これは複数の方法で構築できます。 Sはこれらの選択肢のいずれかで置き換えることができ、Sは常に空で始まります。 εは、プロダクションを終了することを意味します。 Sを置換できるように、他の変数も置換できます(1つだけあり、_bentleyを表すCです)。

したがって、空のSから始めて、最初の選択肢"my"SSに置き換えます

"my"S

Sは変数を表すため、引き続き置換できます。再び「my」を選択するか、εを選択して終了することもできますが、元のステートメントを続けます。 S" "Sに置き換えられることを意味するスペースを選択します

"my "S

次に、Cを選択します

"my "CS

また、Cには交換の選択肢が1つしかありません

"my bentley"S

そして再びSのスペース

"my bentley "S

"my bentley is"S"my bentley is "S"my bentley is blue"S"my bentley is blue"など(Sをεに置き換えると生産が終了します)で、「my bentley is blue」というステートメントを再帰的に作成しました。

これらのプロダクションおよび置換として再帰を考えてください。プロセスの各ステップは、最終結果を生成するために、前のプロセスを置き換えます。 2から5までの再帰的な合計の正確な例では、生産になります

S -> 2 + A
A -> 3 + B
B -> 4 + C
C -> 5 + D
D -> 0

これは

2 + A
2 + 3 + B
2 + 3 + 4 + C
2 + 3 + 4 + 5 + D
2 + 3 + 4 + 5 + 0
14
5
Travis J

やってみます。

方程式a + sumInts(a + 1、b)を実行して、最終的な答えが14であることを示します。

//the sumInts function definition
func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b)
    }
}

Given: a = 2 and b = 5

1) 2 + sumInts(2+1, 5)

2) sumInts(3, 5) = 12
   i) 3 + sumInts(3+1, 5)
   ii) 4 + sumInts(4+1, 5)
   iii) 5 + sumInts(5+1, 5)
   iv) return 0
   v) return 5 + 0
   vi) return 4 + 5
   vii) return 3 + 9

3) 2 + 12 = 14.

さらに質問がある場合はお知らせください。

次の例に、再帰関数の別の例を示します。

男は大学を卒業したばかりです。

tは年単位の時間です。

退職するまでに働いた実際の総年数は、次のように計算できます。

public class DoIReallyWantToKnow 
{
    public int howLongDoIHaveToWork(int currentAge)
    {
      const int DESIRED_RETIREMENT_AGE = 65;
      double collectedMoney = 0.00; //remember, you just graduated college
      double neededMoneyToRetire = 1000000.00

      t = 0;
      return work(t+1);
    }

    public int work(int time)
    {
      collectedMoney = getCollectedMoney();

      if(currentAge >= DESIRED_RETIREMENT_AGE 
          && collectedMoney == neededMoneyToRetire
      {
        return time;
      }

      return work(time + 1);
    }
}

そして、それは誰かを落ち込ませるのにちょうど十分なはずです、笑。 ;-P

5
Bryan

私が学習で知り、再帰を本当に理解している1つの非常に良いヒントは、再帰以外のループ構造の形式を持たない言語の学習に時間を費やすことです。そうすることで、練習で再帰を使用する方法について素晴らしい感触を得ることができます。

http://www.htdp.org/ に従いました。これは、Schemeチュートリアルであるだけでなく、アーキテクチャと設計の観点からプログラムを設計する方法についての優れた入門書でもあります。

しかし、基本的には、ある程度の時間をかける必要があります。再帰を「しっかり」把握しないと、バックトラッキングなどの特定のアルゴリズムは、常に「ハード」または「マジック」に見えます。頑張ってください。 :-D

これがお役に立てば幸いです!

4
Th3Minstr3l

再帰関数を理解する最善の方法は、再帰データ構造を処理するように作られていることを理解することだと思います。ただし、aからbまでの数値の合計を再帰的に計算する元の関数sumInts(a: Int, b: Int)では、再帰的なデータ構造ではないようです...少し変更したバージョンを試してみましょうsumInts(a: Int, n: Int)ここで、nは、追加する数字の数です。

現在、sumIntsはn(自然数)に対して再帰的です。まだ再帰的なデータではありませんか?さて、自然数は、ペアノの公理を使用した再帰的なデータ構造と考えることができます。

enum Natural = {
    case Zero
    case Successor(Natural)
}

したがって、0 =ゼロ、1 =成功(ゼロ)、2 =成功(成功)のようになります。

再帰的なデータ構造を作成すると、関数のテンプレートが作成されます。再帰的でないケースごとに、値を直接計算できます。再帰的なケースの場合想定再帰的な関数は既に動作しており、それを使用してケースを計算しますが、引数を分解します。 Naturalの場合、Succesor(n)の代わりにnを使用すること、またはnの代わりにn - 1を使用することを意味します。

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        // non recursive case
    } else {
        // recursive case. We use sumInts(..., n - 1)
    }
}

これで、再帰関数のプログラミングが簡単になりました。まず、ベースケースn=0。数字を追加しない場合、何を返す必要がありますか?答えはもちろん0です。

再帰的なケースはどうですか? nで始まるa番号を追加し、n-1で機能するsumInts関数がすでにある場合さて、aを追加し、a + 1sumIntsを呼び出す必要があります。

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        return 0
    } else {
        return a + sumInts(a + 1, n - 1)
    }
}

良いことは、低レベルの再帰を考える必要がなくなることです。次のことを確認するだけです。

  • 再帰データの基本ケースの場合、再帰を使用せずに回答を計算します。
  • 再帰的データの再帰的ケースの場合、非構造化データの再帰を使用して回答を計算します。
4
jdinunzio

NisanとSchockenの 関数の実装 に興味があるかもしれません。リンクされたpdfは、無料のオンラインコースの一部です。学生が仮想マシン言語からマシン言語へのコンパイラを記述する仮想マシン実装の2番目の部分について説明します。彼らが提案する関数実装は、スタックベースであるため再帰が可能です。

関数の実装を紹介するには、次の仮想マシンコードを検討してください。

enter image description here

Swiftがこの仮想マシン言語にコンパイルされた場合、次のSwiftコードのブロック:

mult(a: 2, b: 3) - 4

コンパイルします

Push constant 2  // Line 1
Push constant 3  // Line 2
call mult        // Line 3
Push constant 4  // Line 4
sub              // Line 5

仮想マシン言語は、グローバルスタックを中心に設計されています。 Push constant nは、整数をこのグローバルスタックにプッシュします。

1行目と2行目を実行すると、スタックは次のようになります。

256:  2  // Argument 0
257:  3  // Argument 1

256および257はメモリアドレスです。

call multは、戻り行番号(3)をスタックにプッシュし、関数のローカル変数にスペースを割り当てます。

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  0  // local 0

...そしてラベルfunction multに行きます。 mult内のコードが実行されます。そのコードを実行した結果、関数の0番目のローカル変数に格納されている2と3の積を計算します。

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0

マルチからのreturningの直前に、次の行があります。

Push local 0  // Push result

製品をスタックにプッシュします。

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0
260:  6  // product

戻ると、次のことが起こります。

  • スタックの最後の値を0番目の引数のメモリアドレス(この場合は256)にポップします。これはたまたま最も便利な場所です。
  • 0番目の引数のアドレスまでのスタック上のすべてを破棄します。
  • 戻り行番号(この場合は3)に進み、次に進みます。

戻った後、行4を実行する準備ができました。スタックは次のようになります。

256:  6  // product that we just returned

次に、4をスタックにプッシュします。

256:  6
257:  4

subは、仮想マシン言語のプリミティブ関数です。 2つの引数を取り、その結果を通常のアドレス(0番目の引数のアドレス)に返します。

今、私たちは持っています

256:  2  // 6 - 4 = 2

関数呼び出しの仕組みがわかったので、再帰の仕組みを理解するのは比較的簡単です。 マジックなし、ただのスタック。

この仮想マシン言語でsumInts関数を実装しました。

function sumInts 0     // `0` means it has no local variables.
  label IF
    Push argument 0
    Push argument 1
    lte              
    if-goto ELSE_CASE
    Push constant 0
    return
  label ELSE_CASE
    Push constant 2
    Push argument 0
    Push constant 1
    add
    Push argument 1
    call sumInts       // Line 15
    add                // Line 16
    return             // Line 17
// End of function

今、私はそれを呼び出します:

Push constant 2
Push constant 5
call sumInts           // Line 21

コードが実行され、ltefalseを返す停止ポイントに到達します。これは、この時点でスタックがどのように見えるかです:

// First invocation
256:  2   // argument 0
257:  5   // argument 1
258:  21  // return line number
259:  2   // augend
// Second
260:  3   // argument 0
261:  5   // argument 1
262:  15  // return line number
263:  3   // augend
// Third
264:  4   // argument 0
265:  5   // argument 1
266:  15  // return line number
267:  4   // augend
// Fourth
268:  5   // argument 0
269:  5   // argument 1
270:  15  // return line number
271:  5   // augend
// Fifth
272:  6   // argument 0
273:  5   // argument 1
274:  15  // return line number
275:  0   // return value

では、再帰を「ほどき」ましょう。 return 0に進み、15行目に進んで進みます。

271:  5
272:  0

16行目:add

271:  5

17行目:return 5、15行目に進んで進みます。

267:  4
268:  5

16行目:add

267:  9

17行目:return 9、15行目に進んで進みます。

263:  3
264:  9

16行目:add

263:  12

17行目:return 12および15行目に進んで進みます。

259:  2
260:  12

16行目:add

259:  14

17行目:return 14、21行目に進んで進みます。

256:  14

そこにあります。 再帰:修飾されたgoto

4
Jackson

少しトピックから外れていますが、... Googleでrecursionを検索してみてください... :-)


Googleの以前のバージョンでは、次のテキストが返されました(メモリから引用)。

再帰

Recursionを参照

2014年9月10日に、再帰に関するジョークが更新されました。

再帰

もしかして:Recursion


別の返信については、 this answer をご覧ください。

3
Pierre Arnaud

再帰を複数のクローン同じことをしていると考える...

クローンを作成するように要求します[1]:「2〜5の合計数」

+ clone[1]               knows that: result is 2 + "sum numbers between 3 and 5". so he asks to clone[2] to return: "sum numbers between 3 and 5"
|   + clone[2]           knows that: result is 3 + "sum numbers between 4 and 5". so he asks to clone[3] to return: "sum numbers between 4 and 5"
|   |   + clone[3]       knows that: result is 4 + "sum numbers between 5 and 5". so he asks to clone[4] to return: "sum numbers between 5 and 5"
|   |   |   + clone[4]   knows that: result is 5 + "sum numbers between 6 and 5". so he asks to clone[5] to return: "sum numbers between 6 and 5"
|   |   |   |   clone[5] knows that: he can't sum, because 6 is larger than 5. so he returns 0 as result.
|   |   |   + clone[4]   gets the result from clone[5] (=0)  and sums: 5 + 0,  returning 5
|   |   + clone[3]       gets the result from clone[4] (=5)  and sums: 4 + 5,  returning 9
|   + clone[2]           gets the result from clone[3] (=9)  and sums: 3 + 9,  returning 12
+ clone[1]               gets the result from clone[2] (=12) and sums: 2 + 12, returning 14

とボイル!!

3
Christian

すでに多くの良い答えがあります。それでも私は試してみています。
呼び出されると、関数はmemory-spaceを割り当てられ、これはmemory呼び出し元関数の-space。このメモリ空間では、関数は渡されたパラメータ、変数、およびそれらの値を保持します。このmemory-spaceは、関数の終了リターン呼び出しとともに消えます。スタックの概念が進むと、呼び出し元関数のmemory-spaceがアクティブになります。

再帰呼び出しの場合、同じ関数が複数のmemory-spaceを積み重ねます。それで全部です。 stackがコンピューターのメモリー内でどのように機能するかという簡単な考えから、実装で再帰がどのように発生するかがわかります。

3
Gulshan

上記の回答の多くは非常に優れています。ただし、再帰を解決するための便利な手法は、最初に私たちがやりたいことを詳しく説明し、人間がそれを解決するようにコーディングすることです。上記の場合、連続する整数のシーケンスを合計したい(上記の数字を使用):

2, 3, 4, 5  //adding these numbers would sum to 14

さて、これらの行は混乱していることに注意してください(間違っていませんが、混乱している)。

if (a > b) {
    return 0 
}

なぜテストa>b ?、およびwhyreturn 0

人間が行うことをより密接に反映するようにコードを変更しましょう

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When 'a equals b' I'm at the most Right integer, return it
  }
  else {
    return a + sumInts(a: a + 1, b: b)
  }
}

もっと人間らしくできますか?はい!通常、左から右に合計します(2 + 3 + ...)。しかし、上記の再帰は右から左に合計しています(... + 4 + 5)。それを反映するようにコードを変更します(-は少し威圧的かもしれませんが、それほどではありません)

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When I'm at the most Left integer, return it
  }
  else {
    return sumInts(a: a, b: b - 1) + b
  }
}

「遠い」端から開始しているため、この関数はより混乱するかもしれませんが、練習すると自然に感じることができます(そして、別の良い「思考」テクニックです:再帰を解決するときに「両方」の側面を試す)。繰り返しますが、関数は人間(ほとんど?)が行うことを反映しています。すべての左整数の合計を取り、「次の」右整数を追加します。

2
user6085241

再帰は、他の人がそれについて言っていることを読むのをやめるか、それを避けることができるものとして見て、コードを書いたときに、私にとって意味があり始めました。ソリューションの問題を発見し、見ずにソリューションを複製しようとしました。私はどうしようもなく立ち往生したときにのみ解決策を見ました。それから私はそれを複製しようとして戻った。再帰的な問題を特定して解決する方法についての独自の理解と感覚を身につけるまで、複数の問題についてこれを繰り返しました。このレベルに到達したとき、私は問題を作り、解決し始めました。それは私をもっと助けました。時々、物事を自分で試して苦労することでしか学べないことがあります。あなたが「それを得る」まで。

2
Phil

再帰を理解するのに苦労していたので、このブログを見つけました、すでにこの質問を見たので、共有する必要があると思いました。このブログを読む必要があります。これはスタックで説明し、2つの再帰がスタックでどのように機能するかを段階的に説明するのに非常に役立ちます。スタックがどのように機能するかを最初に理解することをお勧めします。これはここで非常によく説明しています: journey-to-the-stack

then now you will understand how recursion works now take a look of this post再帰をステップごとに理解する

enter image description here

そのプログラム:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

enter image description hereenter image description here

2
user6932350

フィボナッチ数列の例を挙げましょう。フィボナッチは

t(n)= t(n-1)+ n;

n = 0の場合1

再帰の仕組みを見てみましょう。t(n)nn-1などに置き換えます。見た目:

t(n-1)= t(n-2)+ n + 1;

t(n-1)= t(n-3)+ n + 1 + n;

t(n-1)= t(n-4)+ n + 1 + n + 2 + n;

t(n)= t(n-k)+ ... +(n-k-3)+(n-k-2)+(n-k-1)+ n;

t(0)=(n-k)1に等しい場合はn-k=0であるため、n=kknに置き換えます。

t(n)= t(n-n)+ ... +(n-n + 3)+(n-n + 2)+(n-n + 1)+ n;

n-nを省略すると:

t(n)= t(0)+ ... + 3 + 2 + 1 +(n-1)+ n;

3+2+1+(n-1)+nは自然数です。 Σ3+2+1+(n-1)+n = n(n+1)/2 => n²+n/2として計算されます

fibの結果は次のとおりです:O(1 + n²) = O(n²)

これが再帰関係を理解する最良の方法