web-dev-qa-db-ja.com

再帰を理解する

学校でrecursionを理解するのに大きな問題があります。教授がそれについて話しているときはいつでも、私はそれを理解しているように見えますが、自分で試してみるとすぐに、それは完全に私の脳を吹き飛ばします。

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

216
Confused

5つの花を含む花瓶をどのように空にしますか?

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

4つの花を含む花瓶をどのように空にしますか?

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

3つの花を含む花瓶をどのように空にしますか?

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

どのように2つの花を含む花瓶を空にしますか?

回答:花瓶が空でない場合は、花を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ループでやったことはありませんか?

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

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

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

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

それが再帰の力です。


上記の花瓶の例は、tail recursionの例です。 tail recursionが意味するのは、再帰関数で、再帰した場合(つまり、関数を再度呼び出した場合)、それが最後に行ったことです。

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

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

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

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

やりたいことは、ツリーをアルファベット順に印刷することです。ツリーの特別なプロパティを考えると、それは簡単です。左の子、次にノードのキャラクター、右の子を印刷するだけです。

Willy-nillyを印刷するだけではないので、印刷する関数を渡します。これは、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」が印刷されていることがわかります。これは実際にアルファベット順になっています。

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

そして、それは再帰の力です:全体の一部を行う方法のみを知ることで(そして再帰を停止するタイミングを知ることで)すべてを行うことができます。

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

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

リュックMコメント:

そのため、この種の回答のバッジを作成する必要があります。おめでとうございます!

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

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

581
tpdi

無限再帰に陥ったため、脳が爆発しました。それは一般的な初心者の間違いです。

信じられないかもしれませんが、あなたはすでに再帰を理解しています。あなたは、機能についての一般的だが欠陥のあるメタファー、つまり出入りする小さな箱に引きずられているだけです。

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

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

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

そのタスクをどのくらい続けますか?脳が爆発するまで永遠に?もちろんそうではありませんが、タスクを完了したと思うときはいつでも、あなたは与えられたポイントで止まります。

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

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

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

「ネットでの再帰の詳細を調べ、 理解するか、最大10ページを読むまで そして www.google.com/search?q=recursionから

すべてを理解するには、これらの本のいずれかを試してみることをお勧めします。

  • Common LISP:シンボリック計算の穏やかな紹介。これは、再帰の非数学的な説明です。
  • 小さな計画者。
35
cfischer

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

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

これに伴う問題は、終了条件がなく、再帰が無期限に繰り返されること、またはシャンプーやお湯がなくなるまで繰り返されることです(外部終了条件、スタックを吹き飛ばすのと同様)。

24
dar7yl

再帰を簡単な用語で説明するのに適した本が必要な場合は、Gödel、Escher、Bach:An Eternal Golden Braid Douglas Hofstadterによる、特に第5章をご覧ください。コンピューターサイエンスと数学の多くの複雑な概念をわかりやすい方法で説明し、一方の説明をもう一方の説明の上に構築するという素晴らしい仕事をしています。以前にこの種の概念にあまり触れたことがなければ、それは非常に衝撃的な本になる可能性があります。

11
Chris Upchurch

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

乗算といえば、これを考えてください。

質問:

A * bとは何ですか?

回答:

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

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

9
S.Lott

この非常に簡単な方法は、再帰を理解するのに役立つと思います。メソッドは、特定の条件が満たされるまで自分自身を呼び出してから戻ります。

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を書き込み、次にwriteNumbers(9)を呼び出して9を書き込み、次にwriteNumber(8)などを呼び出すことです。writeNumbers(1)が1を書き込み、次にwriteNumbers(0)を呼び出して0 buttはwriteNumbers(-1)を呼び出しません。

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

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

次に、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

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

9
Pim Jager

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

8
unbeknown

再帰

メソッド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...
    }
}
5
Sekhat

例で説明しようと思います。

あなたは何を知っています!手段?そうでない場合: 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

お役に立てば幸いです

5
Zoran Zaric

どの本を使用していますか?

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

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

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

4
Uri

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

4
VirtuosiMedia

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でこれをさらに練習できます。

4
Jeff Ancel

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)
3
Rainer Joswig

子は暗黙的に再帰を使用します。たとえば:

ディズニーワールドへのロードトリップ

私たちはまだそこにいますか?(いいえ)

私たちはまだそこにいますか?

私たちはまだそこにいますか?(ほぼ...)

私たちはまだそこにいますか?(SHHHH)

私たちはまだそこにいますか?(!!!!!)

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

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

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

Hofstadterの法則 ソフトウェアプロジェクトに適用することも重要です。

Chomskyによれば、人間の言語の本質は、有限の脳が無限の文法であると考えるものを生成する能力だということです。これにより、彼は私たちが言えることの上限がないことを意味するだけでなく、私たちの言語が持つ文の数に上限がないこと、特定の文のサイズに上限がないことを意味します。チョムスキーは、人間の言語のこのすべての創造性の根底にある基本的なツールは再帰であると主張しています。つまり、あるフレーズが同じタイプの別のフレーズの内部で発生する能力です。 「John's brother's house」と言うと、名詞「house」は名詞句「brother's house」で発生し、その名詞句は別の名詞句「John's brother's house」で発生します。これは非常に理にかなっており、人間の言語の興味深い特性です。

参考文献

3
Paul Sweatte

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

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

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

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

3
dlaliberte

再帰関数を構築する真の数学的な方法は次のとおりです。

1:f(n-1)に対して正しい関数があると想像して、f(n)が正しいようにfを構築します。 2:f(1)が正しいようにfをビルドします。

これが、関数が数学的に正しいことを証明する方法です。これは Induction と呼ばれます。異なるベースケース、または複数の変数でより複雑な関数を使用するのと同等です)。また、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;
}

最後にやることは、無限再帰を停止するための基本ケースを用意することです。 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
FryGuy

痛い。昨年、ハノイの塔を把握しようとしました。 TOHの厄介な点は、単純な再帰の例ではないことです。再帰をネストすると、各呼び出しでタワーの役割も変更されます。理にかなっている唯一の方法は、私の頭の中でリングの動きを文字通り視覚化し、再帰呼び出しが何であるかを言語化することでした。私は単一のリングから始め、次に2、3のリングを使います。実際にインターネットでゲームを注文しました。それを得るために私の頭を割るのに2、3日かかったかもしれません。

2
Jack BeNimble

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

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

また、さまざまなタイプの再帰的な解決策があります。フラクタルやその他の多くに役立つ分割統治アプローチがあります。

また、簡単に問題を解決するために、まず簡単な問題に取り組むことができれば助かります。いくつかの例は、階乗を解き、n番目のフィボナッチ数を生成しています。

参考のために、Robert Sedgewickによるアルゴリズムを強くお勧めします。

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

2
Mark Basmayor

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

それを魔法だと考えてください。あなたが実装しようとしているものと同じ名前の関数があり、サブ問題を与えるとそれがあなたのために解決し、あなたがする必要がある唯一のことはあなたの部分のソリューションをそれと解決することですあなたにあげた。

たとえば、リストの長さを計算します。 magical_lengthを使用して関数magical_lengthとmagicalヘルパーを呼び出しましょう。最初の要素を持たないサブリストを指定すると、サブリストの長さが魔法によって指定されることがわかります。そして、私たちが考える必要があるのは、この情報を仕事に統合する方法だけです。最初の要素の長さは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である場合、答えを何にするかを考える必要があります。したがって、この情報を関数に追加し、これをbase/Edge条件と呼びます。

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
1
reader_1000

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

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

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

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

はっきりしているのかどうかはわかりませんが、教材の実際の使用方法を示すのが好きです。過去につまずいたことがあったからです。

1
PhiLho