再帰を理解する[終了]


225

学校での再帰の理解に大きな問題があります。教授が話をしているときはいつでもそれを理解しているようですが、自分で試してみるとすぐに完全に頭が痛くなります。

私はハノイの塔を一晩中解決しようとしていて、心を完全に吹き飛ばしました。私の教科書は再帰が約30ページしかないので、あまり役に立ちません。このトピックを明確にするのに役立つ本やリソースを知っている人はいますか?


200
再帰を理解するには、まず再帰を理解する必要があります。
ポールトンブリン

40
再帰:再帰を参照
Loren Pechtel 2009

36
@Paul:私は冗談を聞きますが、技術的に間違っているといつも思っていました。アルゴリズムを終了させる基本条件はどこにありますか?これは再帰の基本的な必要条件です。=)
セルジオアコスタ

70
「再帰を理解するには、再帰を理解するまで、再帰を理解する必要があります。」=)
セルジオアコスタ

91
それが役立つかもしれない、この質問を見ていstackoverflow.com/questions/717725/understanding-recursion
オマールKooheji

回答:


598

5つの花を含む花瓶を空にするにはどうすればよいですか。

回答:花瓶が空でない場合は、1つの花を取り出し、次に4つの花を含む花瓶を空にします。

4つの花が入っている花瓶を空にするにはどうすればよいですか。

回答:花瓶が空でない場合は、1つの花を取り出してから、3つの花を含む花瓶を空にします。

3つの花を含む花瓶を空にするにはどうすればよいですか。

回答:花瓶が空でない場合、1つの花を取り出してから、2つの花を含む花瓶を空にします。

2つの花を含む花瓶を空にするにはどうすればよいですか?

回答:花瓶が空でない場合は、花を1つ取り出し、花が1つある花瓶を空にします。

1つの花を含む花瓶を空にするにはどうすればよいですか?

回答:花瓶が空でない場合は、花を1つ取り出してから、花のない花瓶を空にします。

花を含まない花瓶を空にするにはどうすればよいですか?

回答:花瓶が空でない場合は、花を1つ取り出しますが、花瓶は空なので完了です。

それは反復的です。一般化しましょう:

Nの花を含む花瓶を空にするにはどうすればよいですか?

回答:花瓶が空でない場合は、1つの花を取り出してから、N-1花を含む花瓶を空にします。

うーん、コードで確認できますか?

void emptyVase( int flowersInVase ) {
  if( flowersInVase > 0 ) {
   // take one flower and
    emptyVase( flowersInVase - 1 ) ;

  } else {
   // the vase is empty, nothing to do
  }
}

うーん、forループでそれを実行できなかったでしょうか。

なぜ、はい、再帰は反復で置き換えることができますが、多くの場合、再帰はよりエレガントです。

木について話しましょう。コンピュータサイエンスでは、ツリーノードで構成された構造であり、各ノードにはノードでもあるいくつかの子またはnullがあります。バイナリツリーが正確に持つノードで作られた木である2人の子供を、一般的に「左」と「右」と呼ばれます。この場合も、子はノードまたはnullになります。ルートは、他のノードの子ではないノードです。

子に加えてノードが値と数値を持っていると想像してください。そして、あるツリーのすべての値を合計したいとします。

任意の1つのノードの値を合計するには、ノード自体の値を左側の子の値(存在する場合)と右側の子の値(存在する場合)に追加します。ここで、子がnullでなければ、子もノードであることを思い出してください。

したがって、左の子を合計するには、子ノード自体の値をその左の子の値(存在する場合)とその右の子の値(存在する場合)に追加します。

したがって、左の子の左の子の値を合計するには、子ノード自体の値を左の子の値(ある場合)と右の子の値(ある場合)に追加します。

多分あなたは私がこれでどこへ行くのかを予想していて、いくつかのコードを見たいですか?OK:

struct node {
  node* left;
  node* right;
  int value;
} ;

int sumNode( node* root ) {
  // if there is no tree, its sum is zero
  if( root == null ) {
    return 0 ;

  } else { // there is a tree
    return root->value + sumNode( root->left ) + sumNode( root->right ) ;
  }
}

明示的に子をテストしてnullであるかノードであるかを確認するのではなく、nullノードに対して再帰関数でゼロを返すだけであることに注意してください。

したがって、次のようなツリーがあるとします(数値は値であり、スラッシュは子を指し、@はポインターがnullを指すことを意味します)。

     5
    / \
   4   3
  /\   /\
 2  1 @  @
/\  /\
@@  @@

ルート(値5のノード)でsumNodeを呼び出すと、次の結果が返されます。

return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

それを適切に拡張しましょう。sumNodeがあるところはどこでも、それをreturnステートメントの展開に置き換えます。

sumNode( node-with-value-5);
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

return 5 + 4 + sumNode( node-with-value-2 ) + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + 0 + 0
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + sumNode(null ) + sumNode( null ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + 0 + 0 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 
 + 3  ;

return 5 + 4 
 + 2 
 + 1 
 + 3  ;

return 5 + 4 
 + 3
 + 3  ;

return 5 + 7
 + 3  ;

return 5 + 10 ;

return 15 ;

ここで、複合テンプレートの繰り返し適用と見なして、任意の深さと「分岐」の構造をどのように征服したかを見てみましょう。sumNode関数を使用するたびに、単一のif / thenブランチを使用して単一のノードのみを処理し、仕様から直接、ほとんどスリーブを記述した2つの単純なreturnステートメントを処理しましたか?

How to sum a node:
 If a node is null 
   its sum is zero
 otherwise 
   its sum is its value 
   plus the sum of its left child node
   plus the sum of its right child node

それが再帰の力です。


上記の花瓶の例は、末尾再帰の例です。すべての末尾再帰手段は、我々は(我々は再び関数を呼び出した場合、である)再帰場合は再帰関数では、私たちが行った最後のものだったということです。

ツリーの例は末尾再帰ではありませんでした。なぜなら、最後に行ったのは右の子を再帰することでしたが、その前に、左の子を再帰しました。

実際、追加は交換可能であるため、子を呼び出して現在のノードの値を追加する順序はまったく問題になりません。

次に、順序が重要な操作を見てみましょう。ノードのバイナリツリーを使用しますが、今回は、保持される値は数値ではなく文字になります。

ツリーには特別なプロパティがあり、どのノードでも、その文字は左の子が保持する文字の後に(アルファベット順)、右の子が保持する文字の前に(アルファベット順)配置されます。

ここでは、ツリーをアルファベット順に印刷します。ツリーに特別なプロパティがあれば、それは簡単です。左の子、ノードの文字、右の子の順に印刷します。

ただ印刷するだけではなく、印刷する関数を渡します。これは、print(char)関数を持つオブジェクトになります。どのように機能するかを気にする必要はありません。printが呼び出されたときに、どこかで何かが印刷されます。

コードでそれを見てみましょう:

struct node {
  node* left;
  node* right;
  char value;
} ;

// don't worry about this code
class Printer {
  private ostream& out;
  Printer( ostream& o ) :out(o) {}
  void print( char c ) { out << c; }
}

// worry about this code
int printNode( node* root, Printer& printer ) {
  // if there is no tree, do nothing
  if( root == null ) {
    return ;

  } else { // there is a tree
    printNode( root->left, printer );
    printer.print( value );
    printNode( root->right, printer );
}

Printer printer( std::cout ) ;
node* root = makeTree() ; // this function returns a tree, somehow
printNode( root, printer );

この例は、演算の順序が重要であることに加えて、物事を再帰関数に渡すことができることを示しています。私たちがしなければならない唯一のことは、再帰呼び出しのたびに、それを渡し続けることを確認することです。ノードポインターとプリンターを関数に渡し、再帰呼び出しのたびにそれらを「ダウン」させました。

ツリーが次のようになっている場合:

         k
        / \
       h   n
      /\   /\
     a  j @  @
    /\ /\
    @@ i@
       /\
       @@

何を印刷しますか?

From k, we go left to
  h, where we go left to
    a, where we go left to 
      null, where we do nothing and so
    we return to a, where we print 'a' and then go right to
      null, where we do nothing and so
    we return to a and are done, so
  we return to h, where we print 'h' and then go right to
    j, where we go left to
      i, where we go left to 
        null, where we do nothing and so
      we return to i, where we print 'i' and then go right to
        null, where we do nothing and so
      we return to i and are done, so
    we return to j, where we print 'j' and then go right to
      null, where we do nothing and so
    we return to j and are done, so
  we return to h and are done, so
we return to k, where we print 'k' and then go right to
  n where we go left to 
    null, where we do nothing and so
  we return to n, where we print 'n' and then go right to
    null, where we do nothing and so
  we return to n and are done, so 
we return to k and are done, so we return to the caller

したがって、行を表示しただけで印刷された場合:

    we return to a, where we print 'a' and then go right to
  we return to h, where we print 'h' and then go right to
      we return to i, where we print 'i' and then go right to
    we return to j, where we print 'j' and then go right to
we return to k, where we print 'k' and then go right to
  we return to n, where we print 'n' and then go right to

「ahijkn」が実際にアルファベット順に印刷されているのがわかります。

単一のノードをアルファベット順に印刷する方法を知るだけで、ツリー全体をアルファベット順に印刷できます。これは(ノードの値を印刷する前に左の子を印刷し、ノードの値を印刷した後に右の子を印刷する)ことを知っていた(私たちのツリーにはアルファベット順に後の値の左側に値を順序付ける特別なプロパティがあるため)

そして、それが再帰の力です。全体の一部を実行する方法だけを知って(そして再帰をいつ停止するかを知って)すべてのことを実行できるようになります。

ほとんどの言語では、演算子||を思い出してください ( "or")最初のオペランドがtrueの場合の短絡、一般的な再帰関数は次のとおりです。

void recurse() { doWeStop() || recurse(); } 

Luc Mのコメント:

SOは、この種の回答のバッジを作成する必要があります。おめでとう!

ありがとう、Luc!しかし、実際には、この回答を4回以上編集したため(最後の例を追加しますが、主にタイプミスを修正して磨くために、小さなネットブックのキーボードで入力するのは難しい)、それ以上のポイントを得ることができません。 。これは、将来の答えに力を注ぐことをやや思いとどまらせます。

それに関する私のコメントをここで参照してください:https : //stackoverflow.com/questions/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699


35

あなたの脳は無限の再帰に陥ったために爆発しました。これはよくある初心者の間違いです。

信じられないかもしれませんが、あなたはすでに再帰を理解しています。あなたは、関数の一般的な、しかし欠陥のある隠喩に引きずり込まれているだけです。

「ネットでの再帰について詳しく調べる」などのタスクや手順の代わりに考えてください。これは再帰的で、問題はありません。このタスクを完了するには、次のようにします。

a)Googleの「再帰」の結果ページを読む
b)読んだら、最初のリンクをたどって...
a.1)再帰についての新しいページを読む 
b.1)一度読んだら、最初のリンクをたどって...
a.2)再帰についての新しいページを読む 
b.2)一度読んだら、最初のリンクをたどって...

ご覧のとおり、長い間、問題なく再帰的な処理を行ってきました。

その仕事をどれくらい続けますか?あなたの脳が爆破するまで永遠に?もちろん、タスクを完了したと思ったときはいつでも、特定の時点で停止します。

あなたが「ネット上での再帰についてもっと知る」ようにあなたに尋ねるときにこれを指定する必要はありません。なぜならあなたは人間であり、あなたはそれを自分で推測できるからです。

コンピュータはジャックを推測できないため、明示的な終了を含める必要があります。「ネットでの再帰について詳しく調べ、理解できるか、最大で10ページを読んでください」。

また、「再帰」の場合はGoogleの結果ページから開始する必要があると推測しましたが、これもコンピューターでは実行できません。再帰タスクの完全な説明には、明示的な開始点も含める必要があります。

「ネットでの再帰について詳しく調べてください。理解できるまで、またはwww.google.com/search?q=recursionから始めて最大10ページ読んでください

全体を理解するために、以下の本のいずれかを試すことをお勧めします。

  • Common Lisp:シンボリック計算の穏やかな紹介。これは、再帰に関する最も数学的な説明ではありません。
  • 小さな計画家。

6
「関数= I / Oの小さなボックス」というメタファーは、無限クローンを作成するファクトリーがそこにあり、小さなボックスが他の小さなボックスを飲み込むことができると想像する限り、再帰で機能します。
ephemient 2009

2
興味深いので、将来のロボットは何かをグーグルし、最初の10個のリンクを使用して自分で学習します。:) :)
kumar

2
@kumarは、インターネットで既にそれを行っているGoogleではない。
TJ

1
素晴らしい本、推薦ありがとう
Max Koretskyi 2017

「あなたの脳は無限の再帰に陥ったために爆発しました。これはよくある初心者の間違いです。」
スタックアンダーフロー2017年

26

再帰を理解するには、シャンプーボトルのラベルを確認するだけです。

function repeat()
{
   rinse();
   lather();
   repeat();
}

これの問題は、終了条件がなく、再帰が無期限に繰り返されるか、シャンプーまたは温水がなくなるまで繰り返されることです(スタックのブローと同様の外部終了条件)。


6
ありがとうdar7yl-それはいつもシャンプーのボトルで私を困らせました。(私はいつもプログラミングの運命にあったと思います)。指示の最後に「リピート」を追加することに決めた人は会社を数百万人にしたと
思うが

5
私はあなたのrinse()後にあなたを願っていますlather()
CoderDennis

@JakeWilson(末尾呼び出しの最適化が使用されている場合)-確かに。ただし、現状では、これは完全に有効な再帰です。

1
@ dar7ylだから、私のシャンプーボトルは常に空です...
ブランドンリン

11

再帰を簡単に説明するのに適した本が必要な場合は、ダグラスホフスタッターのゲーデル、エッシャー、バッハ:永遠のゴールデンブレイド、特に第5章を参照してください。再帰に加えて、再帰に加えて、コンピュータサイエンスと数学の多くの複雑な概念がわかりやすく、1つの説明が別の説明に基づいています。以前にこのような種類の概念にあまり触れたことがない場合は、かなり驚かれる本になるでしょう。


その後、ホフスタッターの残りの本を見て回ります。今のところ私のお気に入りは、詩の翻訳に関するものです:Le Ton Beau do Marot。正確にはCSの主題ではありませんが、翻訳の本当の意味と意味について興味深い問題を提起します。
RBerteig 2009

9

これは質問というよりクレームです。再帰についてより具体的な質問がありますか?掛け算のように、それは人々が多くについて書くことではありません。

乗算と言えば、これについて考えてください。

質問:

a * bとは何ですか?

回答:

bが1の場合、それはaです。それ以外の場合は、a + a *(b-1)です。

a *(b-1)とは?それを解決する方法については、上の質問を参照してください。


@Andrew Grimm:いい質問ですね。この定義は整数ではなく自然数に対するものです。
S.Lott

9

この非常に単純な方法は、再帰を理解するのに役立つはずです。このメソッドは、特定の条件がtrueになるまで自分自身を呼び出し、次に戻ります。

function writeNumbers( aNumber ){
 write(aNumber);
 if( aNumber > 0 ){
  writeNumbers( aNumber - 1 );
 }
 else{
  return;
 }
}

この関数は、フィードする最初の番号から0まですべての番号を出力します。したがって、次のようになります。

writeNumbers( 10 );
//This wil write: 10 9 8 7 6 5 4 3 2 1 0
//and then stop because aNumber is no longer larger then 0

基本的に起こることは、writeNumbers(10)が10を書き込み、次に9を書き込むwriteNumbers(9)を呼び出してから、writeNumber(8)を呼び出すことです。writeNumbers(1)が1を書き込み、次に0を書き込むwriteNumbers(0)を呼び出すまでbuttはwriteNumbers(-1)を呼び出しません。

このコードは基本的に次のものと同じです。

for(i=10; i>0; i--){
 write(i);
}

次に、forループが基本的に同じことを行うのであれば、なぜ再帰を使用する必要があるのでしょうか。forループをネストする必要があるが、それらがどのくらい深くネストされているかわからない場合は、主に再帰を使用します。たとえば、ネストされた配列からアイテムを出力する場合:

var nestedArray = Array('Im a string', 
                        Array('Im a string nested in an array', 'me too!'),
                        'Im a string again',
                        Array('More nesting!',
                              Array('nested even more!')
                              ),
                        'Im the last string');
function printArrayItems( stringOrArray ){
 if(typeof stringOrArray === 'Array'){
   for(i=0; i<stringOrArray.length; i++){ 
     printArrayItems( stringOrArray[i] );
   }
 }
 else{
   write( stringOrArray );
 }
}

printArrayItems( stringOrArray );
//this will write:
//'Im a string' 'Im a string nested in an array' 'me too' 'Im a string again'
//'More nesting' 'Nested even more' 'Im the last string'

この関数は、100レベルにネストできる配列を取ることができますが、forループを作成すると、100回ネストする必要があります。

for(i=0; i<nestedArray.length; i++){
 if(typeof nestedArray[i] == 'Array'){
  for(a=0; i<nestedArray[i].length; a++){
   if(typeof nestedArray[i][a] == 'Array'){
    for(b=0; b<nestedArray[i][a].length; b++){
     //This would be enough for the nestedAaray we have now, but you would have
     //to nest the for loops even more if you would nest the array another level
     write( nestedArray[i][a][b] );
    }//end for b
   }//endif typeod nestedArray[i][a] == 'Array'
   else{ write( nestedArray[i][a] ); }
  }//end for a
 }//endif typeod nestedArray[i] == 'Array'
 else{ write( nestedArray[i] ); }
}//end for i

ご覧のとおり、再帰的な方法の方がはるかに優れています。


1
笑-JavaScriptを使っていることに気づいた 「関数」を見て、PHPが変数が$で始まっていないことに気づいたのです。次に、varという単語を使用するためにC#を考えましたが、メソッドは関数と呼ばれていません。
ozzy432836 2016年

8

実際、再帰を使用して問題の複雑さを軽減します。簡単に解決できる単純な基本ケースに到達するまで、再帰を適用します。これにより、最後の再帰ステップを解決できます。これにより、元の問題に至るまでのその他すべての再帰的なステップが実行されます。


1
私はこの答えに同意します。秘訣は、基本的な(最も単純な)ケースを特定して解決することです。そして、その問題を最も単純なケース(すでに解決済み)で表現します。
セルジオアコスタ

6

例を挙げて説明します。

あなたは何を知っている!手段?そうでない場合:http : //en.wikipedia.org/wiki/Factorial

3!= 1 * 2 * 3 = 6

ここにいくつかの疑似コードがあります

function factorial(n) {
  if (n==0) return 1
  else return (n * factorial(n-1))
}

だからそれを試してみましょう:

factorial(3)

n 0ですか?

番号!

そのため、再帰を深く掘り下げます。

3 * factorial(3-1)

3-1 = 2

2 == 0ですか?

番号!

より深く行きます!3 * 2 *階乗(2-1)2-1 = 1

1 == 0?

番号!

より深く行きます!3 * 2 * 1 *階乗(1-1)1-1 = 0

0 == 0ですか?

はい!

些細なケースがあります

したがって、3 * 2 * 1 * 1 = 6

お役に立てば幸いです


これは、再帰について考えるのに便利な方法ではありません。初心者が犯すよくある間違いは、それが正しい答えを返すことを単に信頼/証明するのではなく、拒否的な呼び出しの中で何が起こるかを想像しようとすることです。この答えはそれを促進するようです。
ShreevatsaR

再帰を理解するより良い方法は何でしょうか?この方法ですべての再帰関数を見る必要があるとは言いません。しかし、それは私がそれがどのように機能するかを理解するのに役立ちました。
Zoran Zaric 2009

1
[私は-1に投票しませんでした。ところで、このように考えることができます。factorial(n-1)を信頼すると、(n-1)!=(n-1)* ... * 2 * 1が正しく与えられます。 n factorial(n-1)は、n *(n-1) ... * 2 * 1、つまりn!を与えます。または何でも。[もしあなたが再帰関数を自分で書く方法を学ぼうとしているなら、いくつかの関数が何をするかを見るだけでなく。]
ShreevatsaR

私は再帰を説明するときに階乗を使用しましたが、例として失敗する一般的な理由の1つは、説明者が数学を嫌い、それに巻き込まれるためです。(数学を嫌う人がコーディングすべきかどうかは別の問題です)。そのため、私は通常、可能な限り数学以外の例を使用するようにしています。
Tony Meyer、

5

再帰

メソッドAがメソッドAを呼び出しますメソッドAを呼び出します。最終的に、これらのメソッドAの1つは呼び出されず終了しますが、何かがそれ自体を呼び出すため、再帰です。

ハードドライブ上のすべてのフォルダー名を印刷する再帰の例:(c#で)

public void PrintFolderNames(DirectoryInfo directory)
{
    Console.WriteLine(directory.Name);

    DirectoryInfo[] children = directory.GetDirectories();

    foreach(var child in children)
    {
        PrintFolderNames(child); // See we call ourself here...
    }
}

この例の基本ケースはどこですか?
Kunal Mukherjee、

4

どの本を使っていますか?

実際に優れているアルゴリズムの標準的な教科書は、Cormen&Rivestです。私の経験では、再帰を非常によく教えています。

再帰は、プログラミングで把握するのが難しい部分の1つであり、本能は必要ですが、学ぶことはできます。しかし、それは良い説明、良い例、良いイラストを必要とします。

また、一般的に30ページはたくさんあり、単一のプログラミング言語で30ページは混乱しています。一般的な本から再帰を一般的に理解する前に、CまたはJavaで再帰を学ぼうとしないでください。


4

再帰関数は、必要な回数だけ自分自身を呼び出す関数です。何かを複数回処理する必要がある場合に役立ちますが、実際に何回必要になるかはわかりません。ある意味では、再帰関数は一種のループと考えることができます。ただし、ループのように、プロセスが壊れる条件を指定する必要があります。そうしないと、プロセスは無限になります。


4

http://javabat.comは、再帰を練習するための楽しくエキサイティングな場所です。彼らの例はかなり軽量で始まり、広範囲に渡って機能します(これまでにそれを実行したい場合)。注:彼らのアプローチは、実践することによって学ばれます。これは、forループを単に置き換えるために書いた再帰関数です。

forループ:

public printBar(length)
{
  String holder = "";
  for (int index = 0; i < length; i++)
  {
    holder += "*"
  }
  return holder;
}

これは同じことをするための再帰です。(最初のメソッドをオーバーロードして、上記のように使用されることを確認します)。インデックスを維持する別の方法もあります(上記のforステートメントが行う方法と同様)。再帰関数は独自のインデックスを維持する必要があります。

public String printBar(int Length) // Method, to call the recursive function
{
  printBar(length, 0);
}

public String printBar(int length, int index) //Overloaded recursive method
{
  // To get a better idea of how this works without a for loop
  // you can also replace this if/else with the for loop and
  // operationally, it should do the same thing.
  if (index >= length)
    return "";
  else
    return "*" + printBar(length, index + 1); // Make recursive call
}

長い話を短くするために、再帰は少ないコードを書くための良い方法です。後者のprintBarでは、ifステートメントがあることに注意してください。条件に達した場合、再帰を終了して前のメソッドに戻り、前のメソッドに戻ります。printBar(8)で送信すると、********が返されます。これが役に立つかもしれないforループと同じことをする単純な関数の例でそれを望んでいます。ただし、Java Batでこれをもっと練習することができます。


javabat.comは、再帰的に考えるのに役立つ非常に役立つWebサイトです。そこに行って、自分で再帰的な問題を解決することを強くお勧めします。
Paradius

3

再帰関数の作成を調べる真の数学的な方法は次のとおりです。

1:f(n-1)に正しい関数があると想像して、f(n)が正しいようにfを作成してください。2:f(1)が正しいようにfを構築します。

これは、関数が数学的に正確であることを証明する方法です。これを誘導と呼びます。これは、異なる基本ケース、または複数の変数に対するより複雑な関数を持つことと同じです)。また、f(x)がすべてのxに対して正しいと想像することと同じです。

ここで、「単純な」例について説明します。5セントと7セントのコインの組み合わせでxセントにすることが可能かどうかを判断できる関数を作成します。たとえば、2x5 + 1x7で17セントは可能ですが、16セントは不可能です。

ここで、x <nである限り、xセントを作成できるかどうかを通知する関数があるとします。この関数can_create_coins_smallを呼び出します。nの関数の作り方を想像するのはかなり簡単なはずです。次に、関数を作成します。

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins_small(n-7))
        return true;
    else if (n >= 5 && can_create_coins_small(n-5))
        return true;
    else
        return false;
}

ここでの秘訣は、can_create_coinsがnに対して機能するという事実を理解することです。つまり、can_create_coins_smallの代わりにcan_create_coinsを使用すると、次のようになります。

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

最後に行うべきことの1つは、無限の再帰を停止する基本ケースを用意することです。あなたが0セントを作成しようとしている場合、それはコインを持たないことで可能であることに注意してください。この条件を追加すると、次のようになります。

bool can_create_coins(int n)
{
    if (n == 0)
        return true;
    else if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

infinite descentと呼ばれるメソッドを使用して、この関数が常に戻ることを証明できますが、ここでは必要ありません。f(n)はnのより低い値のみを呼び出し、常に最終的に0に達すると想像できます。

この情報を使用してハノイの塔の問題を解決するための秘訣は、n-1個のタブレットをaからb(任意のa / bの場合)に移動し、n個のテーブルをaからbに移動する機能があると想定することです。


3

Common Lispでの単純な再帰的な例:

MYMAPは、リスト内の各要素に関数を適用します。

1)空のリストには要素がないため、空のリストを返します-()とNILはどちらも空のリストです。

2)最初のリストに関数を適用し、リストの残りに対してMYMAPを呼び出し(再帰呼び出し)、両方の結果を組み合わせて新しいリストにします。

(DEFUN MYMAP (FUNCTION LIST)
  (IF (NULL LIST)
      ()
      (CONS (FUNCALL FUNCTION (FIRST LIST))
            (MYMAP FUNCTION (REST LIST)))))

トレースされた実行を見てみましょう。関数を入力すると、引数が出力されます。関数を終了すると、結果が出力されます。再帰呼び出しごとに、出力はレベルでインデントされます。

この例では、リストの各数値(1 2 3 4)でSIN関数を呼び出します。

Command: (mymap 'sin '(1 2 3 4))

1 Enter MYMAP SIN (1 2 3 4)
| 2 Enter MYMAP SIN (2 3 4)
|   3 Enter MYMAP SIN (3 4)
|   | 4 Enter MYMAP SIN (4)
|   |   5 Enter MYMAP SIN NIL
|   |   5 Exit MYMAP NIL
|   | 4 Exit MYMAP (-0.75680256)
|   3 Exit MYMAP (0.14112002 -0.75680256)
| 2 Exit MYMAP (0.9092975 0.14112002 -0.75680256)
1 Exit MYMAP (0.841471 0.9092975 0.14112002 -0.75680256)

これが私たちの結果です:

(0.841471 0.9092975 0.14112002 -0.75680256)

すべての大文字とは何ですか?しかし真剣に、彼らは20年ほど前にLISPで時代遅れになりました。
セバスチャンクロッグ

さて、私はLisp Machineモデルでそれを書きました、それは現在17歳です。実際には、リスナでフォーマットせずに関数を記述し、編集してから、PPRINTを使用してフォーマットしました。それがコードをCAPSに変えました。
Rainer Joswig、2009

3

6歳の人に再帰を説明するには、まず5歳の人に説明してから、1年待ちます。

実際、これは便利な反例です。再帰呼び出しは単純ではなく、難しくないからです。5歳児に再帰を説明するのはさらに難しく、0で再帰を停止することはできますが、0歳児に再帰を説明する簡単な解決策はありません。

再帰を使用して問題を解決するには、最初にそれを同じ方法で解決できる1つ以上のより単純な問題に細分し、次に問題がそれ以上再帰せずに解決できるほど単純な場合は、より高いレベルに戻すことができます。

実際、これは再帰の問題を解決する方法の再帰的な定義でした。


3

子供は暗黙的に再帰を使用します、例えば:

ディズニーワールドへの遠征

まだありますか?(いいえ)

もういますか?(まもなく)

もういますか?(ほぼ...)

もう着いたの?(SHHHH)

もう着きましたか(!!!!!)

その時点で子供は眠りに落ちます...

このカウントダウン関数は簡単な例です:

function countdown()
      {
      return (arguments[0] > 0 ?
        (
        console.log(arguments[0]),countdown(arguments[0] - 1)) : 
        "done"
        );
      }
countdown(10);

ソフトウェアプロジェクトに適用されるホフスタッターの法則も適用されます。

チョムスキーによると、人間の言語の本質は、無限の文法であると彼が考えるものを生成する有限の脳の能力です。これにより、彼は私たちが言うことができる上限がないことだけでなく、私たちの言語が持つ文の数に上限がないこと、特定の文のサイズに上限がないことを意味します。チョムスキーは、人間の言語のこのすべての創造性の根底にある基本的なツールは再帰であると主張しました:1つのフレーズが同じタイプの別のフレーズの中に再出現する能力。「ジョンの兄弟の家」と言うと、名詞句「兄弟の家」にある「家」という名詞があり、その名詞句は別の名詞句「ジョンの兄弟の家」にあります。これは理にかなっています。

参考文献


2

再帰的なソリューションを扱うときは、常に次のことを試みます。

  • 最初に基本ケースを確立します。つまり、階乗の解でn = 1の場合
  • 他のすべてのケースの一般的なルールを考え出すようにしてください

また、さまざまな種類の再帰的解決策があり、フラクタルや他の多くの場合に役立つ分割統治法があります。

また、コツをつかむためだけに、より簡単な問題に最初に取り組むことができる場合にも役立ちます。いくつかの例は、階乗を解いてn番目のフィボナッチ数を生成しています。

参考までに、Robert SedgewickによるAlgorithmsを強くお勧めします。

お役に立てば幸いです。幸運を。


最初に一般的なルールである再帰呼び出しを思い付くのは、あなたが始めたものより「単純」な方がいいのではないかと思います。次に、最も単純なケースに基づいて、基本ケースが明らかになるはずです。それは私が問題を再帰的に解決することについて考える傾向がある方法です。
dlaliberte 2011

2

痛い。昨年ハノイの塔を見つけようとしました。TOHのトリッキーなことは、それが再帰の単純な例ではないことです。ネストされた再帰があり、各呼び出しでのタワーの役割も変更されます。私が理解できる唯一の方法は、心の目でリングの動きを文字通り視覚化し、再帰呼び出しを言葉で表現することでした。最初は1つのリング、2つ、3つのリングから始めます。実際にインターネットでゲームを注文しました。私はそれを手に入れるために私の脳を割るのに多分2日か3日かかった。


1

再帰関数は、呼び出しごとに少し圧縮するスプリングのようなものです。各ステップで、少しの情報(現在のコンテキスト)をスタックに入れます。最後のステップに到達すると、春が解放され、すべての値(コンテキスト)が一度に収集されます。

この比喩が効果的かどうかわからない... :-)

とにかく、少し人工的である古典的な例(非効率的で簡単にフラット化できるので最悪の例である階乗、フィボナッチ、ハノイなど)を超えて(実際のプログラミングのケースで使用することはめったにありません)それが実際に使用されている場所を確認するのは興味深いことです。

非常に一般的なケースは、ツリー(またはグラフですが、一般的にツリーの方が一般的です)を歩くことです。
たとえば、フォルダ階層:ファイルを一覧表示するには、それらを繰り返し処理します。サブディレクトリが見つかった場合、ファイルをリストする関数は、新しいフォルダーを引数として自分自身を呼び出します。この新しいフォルダー(およびそのサブフォルダー!)の一覧から戻ると、次のファイル(またはフォルダー)にコンテキストを再開します。
別の具体的なケースは、GUIコンポーネントの階層を描画するときです。ペインなどのコンポーネントや複合コンポーネントなどを保持するために、ペインなどのコンテナを使用するのが一般的です。ペイントルーチンは、各コンポーネントのペイント関数を再帰的に呼び出します。保持するすべてのコンポーネントのペイント関数を呼び出します。

はっきりしているのかどうかはわかりませんが、過去に偶然見つけた教材の実世界での使用を示すのが好きです。


1

働きバチを考えてください。はちみつを作ろうとします。それはその仕事をし、他の働きバチが蜂蜜の残りを作ることを期待します。そして、ハニカムがいっぱいになると停止します。

魔法だと思ってください。あなたが実装しようとしているものと同じ名前の関数があり、それに副問題を与えるとそれはあなたのためにそれを解決し、あなたがする必要がある唯一のことはあなたの部品の解をそれの解と統合することですあなたにあげた。

たとえば、リストの長さを計算したいとします。magical_lengthで関数magical_lengthとマジカルヘルパーを呼び出しましょう最初の要素を持たないサブリストを与えると、マジックによってサブリストの長さがわかります。次に考える必要があるのは、この情報をどのように私たちの仕事に統合するかです。最初の要素の長さは1で、magic_counterはサブリストn-1の長さを与えるため、合計の長さは(n-1)+ 1-> nです。

int magical_length( list )
  sublist = rest_of_the_list( list )
  sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
  return 1 + sublist_length

ただし、空のリストを指定するとどうなるかを考慮していなかったため、この回答は不完全です。私たちが持っているリストには、常に少なくとも1つの要素があると考えました。したがって、空のリストが与えられ、答えが明らかに0の場合、答えをどうするかを考える必要があります。この情報を関数に追加すると、これはベース/エッジ条件と呼ばれます。

int magical_length( list )
  if ( list is empty) then
    return 0
  else
    sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
    return 1 + sublist_length
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.