学校での再帰の理解に大きな問題があります。教授が話をしているときはいつでもそれを理解しているようですが、自分で試してみるとすぐに完全に頭が痛くなります。
私はハノイの塔を一晩中解決しようとしていて、心を完全に吹き飛ばしました。私の教科書は再帰が約30ページしかないので、あまり役に立ちません。このトピックを明確にするのに役立つ本やリソースを知っている人はいますか?
学校での再帰の理解に大きな問題があります。教授が話をしているときはいつでもそれを理解しているようですが、自分で試してみるとすぐに完全に頭が痛くなります。
私はハノイの塔を一晩中解決しようとしていて、心を完全に吹き飛ばしました。私の教科書は再帰が約30ページしかないので、あまり役に立ちません。このトピックを明確にするのに役立つ本やリソースを知っている人はいますか?
回答:
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
あなたの脳は無限の再帰に陥ったために爆発しました。これはよくある初心者の間違いです。
信じられないかもしれませんが、あなたはすでに再帰を理解しています。あなたは、関数の一般的な、しかし欠陥のある隠喩に引きずり込まれているだけです。
「ネットでの再帰について詳しく調べる」などのタスクや手順の代わりに考えてください。これは再帰的で、問題はありません。このタスクを完了するには、次のようにします。
a)Googleの「再帰」の結果ページを読む b)読んだら、最初のリンクをたどって... a.1)再帰についての新しいページを読む b.1)一度読んだら、最初のリンクをたどって... a.2)再帰についての新しいページを読む b.2)一度読んだら、最初のリンクをたどって...
ご覧のとおり、長い間、問題なく再帰的な処理を行ってきました。
その仕事をどれくらい続けますか?あなたの脳が爆破するまで永遠に?もちろん、タスクを完了したと思ったときはいつでも、特定の時点で停止します。
あなたが「ネット上での再帰についてもっと知る」ようにあなたに尋ねるときにこれを指定する必要はありません。なぜならあなたは人間であり、あなたはそれを自分で推測できるからです。
コンピュータはジャックを推測できないため、明示的な終了を含める必要があります。「ネットでの再帰について詳しく調べ、理解できるか、最大で10ページを読んでください」。
また、「再帰」の場合はGoogleの結果ページから開始する必要があると推測しましたが、これもコンピューターでは実行できません。再帰タスクの完全な説明には、明示的な開始点も含める必要があります。
「ネットでの再帰について詳しく調べてください。理解できるまで、またはwww.google.com/search?q=recursionから始めて最大10ページを読んでください」
全体を理解するために、以下の本のいずれかを試すことをお勧めします。
再帰を理解するには、シャンプーボトルのラベルを確認するだけです。
function repeat()
{
rinse();
lather();
repeat();
}
これの問題は、終了条件がなく、再帰が無期限に繰り返されるか、シャンプーまたは温水がなくなるまで繰り返されることです(スタックのブローと同様の外部終了条件)。
rinse()
後にあなたを願っていますlather()
再帰を簡単に説明するのに適した本が必要な場合は、ダグラスホフスタッターのゲーデル、エッシャー、バッハ:永遠のゴールデンブレイド、特に第5章を参照してください。再帰に加えて、再帰に加えて、コンピュータサイエンスと数学の多くの複雑な概念がわかりやすく、1つの説明が別の説明に基づいています。以前にこのような種類の概念にあまり触れたことがない場合は、かなり驚かれる本になるでしょう。
この非常に単純な方法は、再帰を理解するのに役立つはずです。このメソッドは、特定の条件が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
ご覧のとおり、再帰的な方法の方がはるかに優れています。
実際、再帰を使用して問題の複雑さを軽減します。簡単に解決できる単純な基本ケースに到達するまで、再帰を適用します。これにより、最後の再帰ステップを解決できます。これにより、元の問題に至るまでのその他すべての再帰的なステップが実行されます。
例を挙げて説明します。
あなたは何を知っている!手段?そうでない場合: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
お役に立てば幸いです
再帰
メソッド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...
}
}
再帰関数は、必要な回数だけ自分自身を呼び出す関数です。何かを複数回処理する必要がある場合に役立ちますが、実際に何回必要になるかはわかりません。ある意味では、再帰関数は一種のループと考えることができます。ただし、ループのように、プロセスが壊れる条件を指定する必要があります。そうしないと、プロセスは無限になります。
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でこれをもっと練習することができます。
再帰関数の作成を調べる真の数学的な方法は次のとおりです。
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に移動する機能があると想定することです。
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)
6歳の人に再帰を説明するには、まず5歳の人に説明してから、1年待ちます。
実際、これは便利な反例です。再帰呼び出しは単純ではなく、難しくないからです。5歳児に再帰を説明するのはさらに難しく、0で再帰を停止することはできますが、0歳児に再帰を説明する簡単な解決策はありません。
再帰を使用して問題を解決するには、最初にそれを同じ方法で解決できる1つ以上のより単純な問題に細分し、次に問題がそれ以上再帰せずに解決できるほど単純な場合は、より高いレベルに戻すことができます。
実際、これは再帰の問題を解決する方法の再帰的な定義でした。
子供は暗黙的に再帰を使用します、例えば:
まだありますか?(いいえ)
もういますか?(まもなく)
もういますか?(ほぼ...)
もう着いたの?(SHHHH)
もう着きましたか(!!!!!)
その時点で子供は眠りに落ちます...
このカウントダウン関数は簡単な例です:
function countdown()
{
return (arguments[0] > 0 ?
(
console.log(arguments[0]),countdown(arguments[0] - 1)) :
"done"
);
}
countdown(10);
ソフトウェアプロジェクトに適用されるホフスタッターの法則も適用されます。
チョムスキーによると、人間の言語の本質は、無限の文法であると彼が考えるものを生成する有限の脳の能力です。これにより、彼は私たちが言うことができる上限がないことだけでなく、私たちの言語が持つ文の数に上限がないこと、特定の文のサイズに上限がないことを意味します。チョムスキーは、人間の言語のこのすべての創造性の根底にある基本的なツールは再帰であると主張しました:1つのフレーズが同じタイプの別のフレーズの中に再出現する能力。「ジョンの兄弟の家」と言うと、名詞句「兄弟の家」にある「家」という名詞があり、その名詞句は別の名詞句「ジョンの兄弟の家」にあります。これは理にかなっています。
参考文献
再帰的なソリューションを扱うときは、常に次のことを試みます。
また、さまざまな種類の再帰的解決策があり、フラクタルや他の多くの場合に役立つ分割統治法があります。
また、コツをつかむためだけに、より簡単な問題に最初に取り組むことができる場合にも役立ちます。いくつかの例は、階乗を解いてn番目のフィボナッチ数を生成しています。
参考までに、Robert SedgewickによるAlgorithmsを強くお勧めします。
お役に立てば幸いです。幸運を。
痛い。昨年ハノイの塔を見つけようとしました。TOHのトリッキーなことは、それが再帰の単純な例ではないことです。ネストされた再帰があり、各呼び出しでのタワーの役割も変更されます。私が理解できる唯一の方法は、心の目でリングの動きを文字通り視覚化し、再帰呼び出しを言葉で表現することでした。最初は1つのリング、2つ、3つのリングから始めます。実際にインターネットでゲームを注文しました。私はそれを手に入れるために私の脳を割るのに多分2日か3日かかった。
再帰関数は、呼び出しごとに少し圧縮するスプリングのようなものです。各ステップで、少しの情報(現在のコンテキスト)をスタックに入れます。最後のステップに到達すると、春が解放され、すべての値(コンテキスト)が一度に収集されます。
この比喩が効果的かどうかわからない... :-)
とにかく、少し人工的である古典的な例(非効率的で簡単にフラット化できるので最悪の例である階乗、フィボナッチ、ハノイなど)を超えて(実際のプログラミングのケースで使用することはめったにありません)それが実際に使用されている場所を確認するのは興味深いことです。
非常に一般的なケースは、ツリー(またはグラフですが、一般的にツリーの方が一般的です)を歩くことです。
たとえば、フォルダ階層:ファイルを一覧表示するには、それらを繰り返し処理します。サブディレクトリが見つかった場合、ファイルをリストする関数は、新しいフォルダーを引数として自分自身を呼び出します。この新しいフォルダー(およびそのサブフォルダー!)の一覧から戻ると、次のファイル(またはフォルダー)にコンテキストを再開します。
別の具体的なケースは、GUIコンポーネントの階層を描画するときです。ペインなどのコンポーネントや複合コンポーネントなどを保持するために、ペインなどのコンテナを使用するのが一般的です。ペイントルーチンは、各コンポーネントのペイント関数を再帰的に呼び出します。保持するすべてのコンポーネントのペイント関数を呼び出します。
はっきりしているのかどうかはわかりませんが、過去に偶然見つけた教材の実世界での使用を示すのが好きです。
働きバチを考えてください。はちみつを作ろうとします。それはその仕事をし、他の働きバチが蜂蜜の残りを作ることを期待します。そして、ハニカムがいっぱいになると停止します。
魔法だと思ってください。あなたが実装しようとしているものと同じ名前の関数があり、それに副問題を与えるとそれはあなたのためにそれを解決し、あなたがする必要がある唯一のことはあなたの部品の解をそれの解と統合することですあなたにあげた。
たとえば、リストの長さを計算したいとします。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