再帰関数のしくみを理解する


115

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

わからないことがわからない場合は、間違った質問をしたり、間違った質問をしたりする傾向があることを理解している。私にとって再帰的な電球をオンにするのに役立つ少しの知識!

ここに関数があります(構文は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、私は永遠にあなたの借金の中にいるでしょう。


7
再帰はプログラミングの概念の1つであり、コードよりも数学的な用語で理解する方がはるかに簡単です。ここに
blgt '09 / 09/05

5
LearnYouARecursion、世界クラスの教授からの完全な問題セット!
recursion.ninja 14

15
私は持っている Googleの検索ボックスに「再帰」と入力して、あなたを促すように。それらのイースターエッグの一つ。私はあなたのために驚きを台無しにしないでしょう。
フローリス


回答:


107

私が考えます混乱が何度も呼び出されて「同じ機能」として、それを考えてから生じています。「同じ関数の多数のコピーが呼び出される」と考えると、より明確になる可能性があります。

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

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

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

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

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

(編集:また、私が言及する関数のコピーのスタックは、実際のメモリを消費する実物であり、プログラムが大きくなりすぎるとプログラムがクラッシュすることにも注意してください。コンパイラは、いくつかの点で最適化できます。ケース、ただしスタックスペースの枯渇は、多くの言語での再帰関数の重要かつ不幸な制限です)


7
Catfish_Man:釘付けにしたと思います!同じ機能のいくつかの「コピー」としてそれを考えることは完全に理にかなっています。私はまだ頭を抱えていますが、あなたは私を正しい道に送り出したと思います!忙しい1日の時間を割いて、他のプログラマを助けてくれてありがとう!あなたの答えを正解としてマークします。すてきな一日を!
Jason Elwood 2014

13
これは良いアナロジーです。ただし、各「コピー」は実際にはまったく同じコードであるため、文字どおりに取りすぎないように注意してください。コピーごとに異なるのは、作業しているすべてのデータです。
Tim B

2
私はそれをコピーとして考えることにあまり満足していません。より直感的な説明は、関数自体(コード、その機能)と、スタックフレーム/実行コンテキストが関連付けられている関数呼び出し(その関数のインスタンス化)を区別することです。関数はローカル変数を所有しておらず、関数が呼び出される(呼び出される)ときにインスタンス化されます。しかし、これは再帰の紹介として役立つと思います
Thomas

5
正しい用語は、関数の呼び出しがいくつかあるということです。各呼び出しには、変数との独自のインスタンスがあります。ab
セオドアノーベル2014

6
はい、この回答に追加できるかなりの精度があります。「関数のインスタンス」と「単一の関数の呼び出しのアクティブ化レコード」の違いを意図的に省略しました。これは、問題を理解するのに実際には役立たない余分な概念的な負荷であるためです。それは他の問題を理解するに役立ちますので、それはまだ他の場所でも有用な情報です。これらのコメントはそれのための素晴らしい場所のように見えます:)
Catfish_Man

130

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を返しますが、これは最終値ではなく、コンピューターはその0に5、次に結果に4、3、2を追加する必要があるためです。私たちのコンピュータの考え。再帰では、コンピューターは再帰呼び出しを計算する必要があるだけでなく、再帰呼び出しによって返された値をどう処理するかを覚えておく必要があることに注意してください。この種の情報が保存されるスタックと呼ばれるコンピューターのメモリの特別な領域があり、このスペースは制限されており、再帰的すぎる関数はスタックを使い果たす可能性があります。これは、最も愛されているWebサイトにその名前を与えるスタックオーバーフローです。

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

2.各反復で 'a'の値を出力すると、2、3、4、5と予想される値が得られます(この時点で、5 + 1> bで最初の条件を満たす:a> b)。 14の値がどのように達成されるかはわかりません。

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


3
この素晴らしい答えをマイケルに書いてくれてありがとう!+1!
Jason Elwood 2014

9
@JasonElwood sumInts実際に「コンピュータの考え」を書き留めるように変更すると役立つかもしれません。このような関数の手を書いたら、おそらく「理解した」ことでしょう。
Michael Le BarbierGrünewald2014

4
これは良い答えですが、「スタック」と呼ばれるデータ構造で関数のアクティブ化が行われる必要はないことに注意します。再帰は、継続渡しスタイルによって実装できます。この場合、スタックはまったくありません。スタックは1つに過ぎません-特に効率的であり、したがって一般的な使用法では-継続の概念の具体化です。
Eric Lippert、2014

1
@EricLippert再帰性を実装するために使用される手法自体は興味深いトピックですが、「仕組み」を理解したいOPがさまざまなメカニズムに触れることが役立つかどうかはわかりません。スタイルまたは拡張ベースの言語(例えばTeXとM4)を通過継続が本質的に難しく、より一般的なプログラミングパラダイムよりもありませんが、私はこれらの「エキゾチック」を標識することにより、誰と少し犯罪ではないでしょう白い嘘「それはいつもに起こるようなSHOULDスタック」 OPが概念を理解するのに役立ちます。(また、一種のスタックが常に含まれます。)
Michael Le BarbierGrünewald'14

1
ソフトウェアが実行していたことを記憶し、関数を再帰的に呼び出して、元の状態に戻るには、何らかの方法が必要です。このメカニズムはスタックのように機能するため、他のデータ構造が使用されている場合でも、スタックと呼ぶと便利です。
Barmar

48

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

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

手順を紹介しましょう。

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 

2
非常によく言えば、ロブ。非常に明確で理解しやすい方法で記述しました。お時間をいただきありがとうございます!
Jason Elwood 2014

3
これは、何が起こっているかを最も明確に表したものであり、理論や技術的な詳細には触れず、実行の各ステップを明確に示しています。
ブライアン

2
私は嬉しいです。:)これらのことを説明するのは必ずしも容易ではありません。お褒めの言葉をありがとうございます。
Rob

1
+1。これは、特にあなたの最後の構造例で私がそれを説明する方法です。起こっていることを視覚的に展開することは役に立ちます。
KChaloux 2014

40

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

ここで指定したコードは、次の問題を解決します。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 + 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. 答えを得るにはaを追加します。

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

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 + sumInts(6, 5)戻るために計算することができます5。次に、それを最上位の呼び出し元に返します。

ここで値5がどのように形成されたかに注目してください。まず、へのアクティブな呼び出しを1つ始めましたsumInts。それが別の再帰呼び出しを引き起こし、その呼び出しによって返された値がに情報を返しました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)に0を返しますsumInts(now has a value for6、5), namely 0. It then returns5 + 0 = 5`。
  • sumInts(4, 5)これで、の値sumInts(5, 5)、つまり5が得られます4 + 5 = 9。次に、を返します。

言い換えると、返される値は、一度に1つずつ値を合計し、毎回特定の再帰呼び出しによって返される1つの値を取得sumIntsして、の現在の値に追加することによって形成されますa。再帰が終了すると、最も深い呼び出しは0を返します。ただし、その値はすぐに再帰呼び出しチェーンを終了しません。代わりに、1つ上のレイヤーの再帰呼び出しに値を渡します。このようにして、各再帰呼び出しはさらに1つの数値を加算し、それをチェーンの上位に返し、全体の合計で終わります。演習として、これをでトレースしてみてくださいsumInts(2, 5)。これは、最初にやりたかったことです。

お役に立てれば!


3
このような包括的な答えを共有するために忙しい一日の時間を割いていただきありがとうございます!ここにはたくさんの素晴らしい情報があり、再帰的な関数を理解するのに役立ち、将来この投稿に遭遇した他の人を確実に助けるでしょう。改めて感謝し、素晴らしい一日を!
Jason Elwood 2014

22

ここまでは良い答えがいくつかありますが、もう1つ、別の方法で説明します。

最初に、私はあなたが興味深いと思うかもしれない単純な再帰的アルゴリズムに関する多くの記事を書きました。見る

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

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

これらは最新のものから順に並んでいるので、下から始めます。

第二に、これまでのところ、すべての回答は、関数のアクティブ化を検討することにより、再帰的なセマンティクスを説明しています。つまり、各呼び出しは新しいアクティベーションを行い、再帰呼び出しはこのアクティベーションのコンテキストで実行されます。それはそれを考える良い方法ですが、同等の別の方法があります。それは、スマートテキストの検索と置換です。

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

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

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

次にs(2,5) 、呼び出しをテキストで関数本体に置き換え、次にawith 2およびbwithを置き換えることで評価し5ます。

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

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

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

今度は、すべての偽の条件文を代替テキストで置き換え、すべての真の条件文を結果で置き換えます。誤った条件しかないため、テキストでその式を代替に置き換えます。

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

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

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

次に、検索と置換を行います。今回は、b 、3for a5forの呼び出しの本文を使用します。呼び出しの代わりを括弧で囲みます。

---> 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、置き換える必要のないステップに到達し、算術のルールを適用できることに注目してください。


良い例ですが、より複雑な計算に取り掛かると心が痛くなります。例えば。バイナリツリーで共通の祖先を見つける。
CodeYogi、2015年

11

私が通常再帰関数がどのように機能するかを理解する方法は、基本ケースを調べて逆に作業することです。この関数に適用されるテクニックは次のとおりです。

まずベースケース:

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)

関数がどのように動作するかについては、呼び出しを返す順序で追跡することで結論に達したことに注意してください。


5

やってみます。

方程式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

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

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

これは、「私の車はベントレーです。私の車は青いです」という記述の場合は異なる場合があります。その場合、自動車の2番目の状況での置換は「ベントレー」であり、「私のベントレーは青い」になります。これらのタイプの置換は、文脈自由文法を通じてコン​​ピュータサイエンスで数学的に説明されていますます。

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

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

これは複数の方法で構築できます|。それぞれが選択肢があることを意味します。Sこれらの選択肢のいずれかで置き換えることができ、Sは常に空から始まります。ε生産を終了することを意味します。S置換できるのと同じように、他の変数も置換できます(1つだけあり、C「ベントレー」を表す)。

始まるのでS、空であること、および最初の選択でそれを置き換える"my"S Sになり

"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に置き換える)、我々は再帰的に「私のベントレーは青である」私たちのステートメントを構築してきました。

再帰は、これらのプロダクションおよび置換と考えてください。プロセスの各ステップは、最終結果を生成するために、その前のステップを置き換えます。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

有限状態オートマトンまたは文脈自由文法が、再帰に関する最初の直感を構築するのに役立つ最良の例であるかどうかはわかりません。それらは素晴らしい例ですが、以前にCSのバックグラウンドを持っていなかったプログラマーには、なじみがないかもしれません。
2014

4

再帰関数を理解する最良の方法は、それらが再帰的なデータ構造を処理するように作られていることを理解することだと思います。しかし、あなたの元の関数でsumInts(a: Int, b: Int)再帰的計算からの数値の合計ということaにはb、再帰的なデータ構造...レッツ・トライ少し変更したバージョンではないように思わあなたが追加しますどのように多くの数字です。sumInts(a: Int, n: Int)n

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

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

つまり、0 =ゼロ、1 = Succesor(ゼロ)、2 = Succesor(Succesor(ゼロ))などです。

再帰的なデータ構造を作成すると、関数のテンプレートが作成されます。非再帰的なケースごとに、値を直接計算できます。再帰的なケースでは、再帰的な関数がすでに機能していると想定し、それを使用してケースを計算しますが、引数を分解します。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、すでにsumInts機能する作業関数がありn-1ますか?まあ、我々は、追加する必要がありますaし、[起動sumIntsa + 1、私たちはで終わるので:

// 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

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

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

ここに画像の説明を入力してください

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

256257メモリアドレスです。

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

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

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

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

returnmultから始める直前に、次の行に気づくでしょう:

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

コードが実行され、lte戻り値の停止点に到達しますfalse。この時点でスタックは次のようになります。

// 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

では、再帰を「ほどいて」いきましょう。return0から15行目に移動して進みます。

271:  5
272:  0

行16: add

271:  5

17行目:return5行目、15行目へ移動して進みます。

267:  4
268:  5

行16: add

267:  9

17行目:return9行目、15行目に進んでください。

263:  3
264:  9

行16: add

263:  12

17行目:return12行目、15行目に進んでください。

259:  2
260:  12

行16: add

259:  14

17行目:return14行目、21行目に進んでください。

256:  14

そこにあります。再帰:栄光goto


4

再帰の学習と理解を深める上で出会った本当に良いヒントの1つは、再帰以外の形でループ構造を持たない言語の学習に時間をかけることです。そうすれば、練習を通じて再帰を使用する方法について素晴らしい感触を得ることができます。

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

しかし、基本的には、ある程度の時間を投資する必要があります。再帰を「しっかり」把握していないと、バックトラックなどの特定のアルゴリズムは、常に「難しい」または「魔法」にさえ見えるでしょう。だから、辛抱して。:-D

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


3

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

再帰呼び出しの場合、同じ関数が複数のメモリ空間を積み重ねます。それで全部です。コンピュータのメモリ内でスタックがどのように機能するかという単純な概念は、実装で再帰がどのように発生するかという概念を理解してくれるはずです。


3

少しトピックから外れているようですが、Googleで再帰を検索してみてください。例によって、その意味がわかります:-)


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

再帰

再帰を参照

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

再帰

もしかして:再帰


別の返信については、この回答を参照してください。


3

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

あなたはクローンを作るように頼みます[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

そしてボイラー!!


2

上記の答えの多くは非常に良いです。ただし、再帰を解決するための便利な手法は、最初に実行したいことを詳しく説明し、人間が解決するようにコーディングすることです。上記の場合、一連の連続した整数を合計します(上記の数値を使用)。

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

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

if (a > b) {
    return 0 
}

なぜテスト a>b?そしてなぜreturn 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

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

then now you will understand how recursion works now take a look of this post再帰を段階的に理解する

ここに画像の説明を入力してください

そのプログラム:

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)

ここに画像の説明を入力してください ここに画像の説明を入力してください


2

再帰は、他の人が言っていることを読むのをやめるか、回避できるものと見なしてコードを書いただけで、理にかなっています。解決策に問題が見つかり、見ないで解決策を複製しようとしました。どうしようもなく動けなくなったときだけ、解決策を見ました。それから私はそれを複製しようとして戻ってきました。再帰的な問題を特定して解決する方法について自分自身の理解と感覚を養うまで、私は複数の問題についてこれを繰り返しました。このレベルに達したとき、私は問題を構成し、それらを解決し始めました。それは私をさらに助けました。時々、自分で試して苦労することでしか物事を学べないことがあります。「手に入れる」まで。


0

フィボナッチシリーズの例を挙げましょう。フィボナッチは

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

n = 0の場合は1

私はちょうど置き換え、再帰がどのように動作するか見てみましょうnt(n)n-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(nk)+ ... +(nk-3)+(nk-2)+(nk-1)+ n;

あれば、私たちは知っているt(0)=(n-k)と等しく1、その後はn-k=0そうn=k、我々は交換するkn

t(n)= t(nn)+ ... +(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²)

これは再帰的な関係を理解するための最良の方法です

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.