再帰の考え方は、現実の世界ではあまり一般的ではありません。そのため、初心者のプログラマにとっては少し混乱するようです。とはいえ、彼らは徐々にコンセプトに慣れてくると思います。だから、彼らがアイデアを簡単に把握するための良い説明は何ですか?
再帰の考え方は、現実の世界ではあまり一般的ではありません。そのため、初心者のプログラマにとっては少し混乱するようです。とはいえ、彼らは徐々にコンセプトに慣れてくると思います。だから、彼らがアイデアを簡単に把握するための良い説明は何ですか?
回答:
再帰を説明するために、私は異なる説明の組み合わせを使用します。
まず第一に、Wolfram | AlphaはWikipediaよりも簡単な用語で定義しています。
特定の数学的操作を繰り返すことにより各用語が生成されるような式。
あなたの学生(または私は学生を言うよにあなたが今から、あまりにも説明者)が、少なくともいくつかの数学的背景を持っている場合、彼らは明らかに、すでにシリーズとの彼らの概念研究することによって、再帰が発生しました再帰性とその再発の関係を。
開始するための非常に良い方法は、シリーズでデモンストレーションし、それが非常に単純に再帰についてであると言うことです:
通常、あなたはせいぜい「ハァッ、ホェーテブ」を使用しますが、それは彼らがまだそれを使用していないためです。
残りの部分については、実際には、ポインターに関して指摘した質問に対する私の回答の補遺で示したものの詳細なバージョンです(悪戯)。
この段階では、生徒は通常、画面に何かを印刷する方法を知っています。Cを使用していると仮定すると、彼らはwrite
or を使用して単一の文字を印刷する方法を知っていますprintf
。また、制御ループについても知っています。
私は通常、いくつかの反復的で単純なプログラミングの問題を解決するまで頼ります。
階乗
階乗は理解するための非常に単純な数学概念であり、実装はその数学表現に非常に近いものです。ただし、最初は取得できない場合があります。
アルファベット
アルファベット版は、再帰文の順序について考えるように教えるのに興味深いです。ポインタのように、それらはあなたにランダムに線を投げます。ポイントは、ループが条件を修正することのいずれかによって反転することができます実現にそれらをもたらすことですOR justあなたの関数内の文の順序を反転させて。それは彼らにとって視覚的なものであるため、アルファベットの印刷が役立ちます。呼び出しごとに1文字を出力し、それ自体を再帰的に呼び出して次の(または前の)文字を書き込む関数を作成するだけです。
FPファン、出力ストリームに印刷することは今のところ副作用であるという事実をスキップしてください... FPの面倒なことはやめましょう。(ただし、リストをサポートする言語を使用する場合は、各反復でリストに連結し、最終結果を出力してください。通常、Cで開始します。残念ながら、この種の問題や概念には最適ではありません) 。
べき乗
べき乗の問題は、やや難しくなります(学習のこの段階では)。明らかに、概念は階乗の場合とまったく同じであり、複数のパラメーターがあることを除いて、追加の複雑さはありません... そして、それは通常、人々を混乱させ、最初に彼らを捨てるのに十分です。
その単純な形式:
繰り返しによってこのように表現できます:
もっと強く
これらの簡単な問題がチュートリアルで示され、再実装されたら、もう少し難しい(しかし非常に古典的な)演習を行うことができます。
注:繰り返しますが、これらのいくつかは実際にはそれほど難しくありません...まったく同じ角度またはわずかに異なる角度から問題にアプローチします。しかし、練習すれば完璧になります。
参照
一部の読書は決して痛くない。まあそれは最初はそうなるでしょう、そして彼らはさらに失われたと感じるでしょう。それはあなたの上で成長し、ある日あなたが最終的にそれを手に入れることに気付くまであなたの頭の後ろに座っているようなものです。そして、あなたはあなたが読んだこれらのものを振り返ります。現時点では、Wikipediaの再帰、コンピューターサイエンスの再帰、 および再帰関係のページで対応します。
レベル/深さ
学生にコーディング経験があまりないと仮定して、コードスタブを提供します。最初の試行の後、再帰レベルを表示できる印刷機能を提供します。レベルの数値を印刷すると役立ちます。
引き出しとしてのスタック図
印刷結果(またはレベルの出力)をインデントすることも役立ちます。プログラムが実行していることの別の視覚的表現を提供し、引き出しやファイルシステムエクスプローラーのフォルダーなどのスタックコンテキストを開閉します。
再帰的な頭字語
学生が既にコンピューターカルチャーに精通している場合、再帰的な頭字語を使用した名前のプロジェクト/ソフトウェアを既に使用している可能性があります。それはしばらくの間、特にGNUプロジェクトで行われている伝統です。以下に例を示します。
再帰的:
相互再帰的:
彼らに彼ら自身のものを考え出させてください。
同様に、Googleの再帰的な検索修正のように、再帰的なユーモアが多数発生します。再帰の詳細については、この回答を参照してください。
人々が通常苦労し、あなたが答えを知る必要があるいくつかの問題。
なんで?
どうしてそうするか?良いが非自明な理由は、問題をそのように表現する方が簡単な場合が多いからです。それほど良くはないが明らかな理由は、多くの場合、タイピングが少なくて済むことです(ただし、再帰を使用するだけですっごくl33tに感じさせないでください...)。
一部の問題は、再帰的なアプローチを使用すると間違いなく簡単に解決できます。通常、分割統治パラダイムを使用して解決できる問題は、多分岐再帰アルゴリズムに適合します。
Nとは何ですか??
私n
または(変数の名前に関係なく)毎回異なるのはなぜですか?初心者は通常、変数とパラメーターが何であるかを理解するのに問題があり、n
プログラムで名前が付けられたものが異なる値を持つことができる方法を理解します。そのため、この値が制御ループまたは再帰にある場合、それはさらに悪いことです!すべての場所で同じ変数名を使用しないでください。また、パラメーターが単なる変数であることを明確にしてください。
終了条件
終了条件を判断するにはどうすればよいですか?それは簡単です、彼らにステップを大声で言わせてください。たとえば、5から階乗開始の場合、4、次に...まで0です。
悪魔は細部に宿る
テールコールの最適化など、初期段階では話をしないでください。TCOはいいのですが、最初は気にしません。彼らに効果的な方法で彼らの頭をプロセスに巻き付けるための時間を与えてください。後で彼らの世界を再び粉砕してください、しかし彼らに休憩を与えてください。
同様に、コールスタックとそのメモリ消費、そして... スタックオーバーフローについての最初の講義から直接話さないでください。私はしばしば、この段階でループをほとんど書くことができないときに、再帰について知っておくべきことすべてについて50枚のスライドがある講義を個人的に教えています。これは、後で参照がどのように役立つかを示す良い例ですが、今はあなたを深く混乱させています。
しかし、しばらくして、反復ルートまたは再帰ルートを使用する理由があることを明確にしてください。
相互再帰
関数は再帰的であり、複数の呼び出しポイント(8クイーン、ハノイ、フィボナッチ、または掃海艇の探索アルゴリズム)を持つことさえできることがわかりました。しかし、相互再帰呼び出しはどうでしょうか?ここでも数学から始めてください。f(x) = g(x) + h(x)
どこでg(x) = f(x) + l(x)
、h
そしてl
ただやるだけです。
数学的シリーズから始めると、契約が式によって明確に定義されるため、記述と実装が容易になります。たとえば、ホフスタッターの女性と男性のシーケンス:
ただし、コードに関しては、相互再帰的なソリューションを実装するとコードが重複することが多く、単一の再帰的な形式に合理化する必要があることに注意してください(Peter NorvigのSolving Every Sudoku Puzzleを参照してください)。
static unsigned int vote = 1;
私から得ます。静的なユーモアを許してください:)これはこれまでのベストアンサーです。
それを使用する方法、使用するタイミング、悪いデザインを避ける方法は知っておくことが重要です。これはあなた自身でそれを試して、何が起こるかを理解する必要があります。
知っておく必要がある最も重要なことは、決して終わらないループを取得しないように非常に注意することです。pramodc84からの質問への答えには、次のような欠点があります。終わらない...
再帰関数は、常に自分自身を呼び出す必要があるかどうかを判断するための条件をチェックする必要があります。
再帰を使用する最も典型的な例は、静的な深さ制限のないツリーで作業することです。これは、再帰を使用する必要があるタスクです。
a
まだ間接的に(を呼び出すことによりb
)自身を呼び出します。
再帰プログラミングは、問題を段階的に減らして、それ自体のバージョンを解決しやすくするプロセスです。
すべての再帰関数は次の傾向があります。
ステップ2が3の前で、ステップ4が自明(連結、合計、またはなし)の場合、これにより末尾再帰が有効になります。現在のステップを完了するには、問題のサブドメインからの結果が必要になる可能性があるため、ステップ2はしばしばステップ3の後に来る必要があります。
単純な二分木をたどってください。トラバーサルは、必要に応じて、事前順序、順序どおり、または順序どおりに行うことができます。
B
A C
予約注文:BAC
traverse(tree):
visit the node
traverse(left)
traverse(right)
順序どおり:ABC
traverse(tree):
traverse(left)
visit the node
traverse(right)
注文後:ACB
traverse(tree):
traverse(left)
traverse(right)
visit the node
非常に多くの再帰的な問題は、マップ操作またはフォールドの特定のケースです。これらの2つの操作だけを理解すると、再帰の適切なユースケースを十分に理解できます。
OPは、現実世界には再帰は存在しないと言っていましたが、私は違います。
ピザを切るという現実の「操作」を考えてみましょう。あなたはピザをオーブンから取り出し、それを提供するために半分にカットし、それらの半分を半分にカットし、それらの結果の半分を再び半分にカットする必要があります。
目的の結果(スライスの数)が得られるまで、何度も何度も実行するピザをカットする操作。そして、議論のために、カットされていないピザはスライスそのものだとしましょう。
Rubyの例を次に示します。
def cut_pizza(existing_slices、desired_slices) if existing_slices!= desired_slices #みんなを養うのに十分なスライスがまだないので、 #ピザのスライスをカットしているので、その数が倍になります new_slices = existing_slices * 2 #これは再帰呼び出しです cut_pizza(new_slices、desired_slices) 他に #必要な数のスライスがあるので、戻る #ここで再帰を続ける代わりに existing_slicesを返します 終わり 終わり pizza = 1#ピザ全体、「1スライス」 cut_pizza(pizza、8)#=> 8を取得します
したがって、現実世界の操作はピザをカットし、再帰はあなたが望むものを得るまで同じことを何度も繰り返しています。
再帰関数で実装できるトリミングは次のとおりです。
ファイル名に基づいてファイルを検索するプログラムを作成し、それが見つかるまで自分自身を呼び出す関数を作成することをお勧めします。署名は次のようになります。
find_file_by_name(file_name_we_are_looking_for, path_to_look_in)
したがって、次のように呼び出すことができます。
find_file_by_name('httpd.conf', '/etc') # damn it i can never find apache's conf
私の意見では単純にプログラミングの仕組みであり、重複を巧みに除去する方法です。変数を使用してこれを書き換えることができますが、これは「より良い」ソリューションです。不思議なことや難しいことは何もありません。あなたは再帰関数のカップルを書きます、それはをクリックしますhuzzahあなたのプログラミングツールボックス内の別の機械的なトリックを。
余分なクレジット上記のcut_pizza
例では、2の累乗ではないスライス数(2または4または8または16)を要求すると、スタックレベルの深すぎるエラーが発生します。誰かが10個のスライスを要求した場合、永遠に実行されないように変更できますか?
さて、このシンプルで簡潔なものにしようと思っています。
再帰関数は、それ自体を呼び出す関数です。再帰関数は次の3つの要素で構成されます。
再帰的なメソッドを記述する最良の方法は、反復しようとするプロセスのループを1つだけ処理する単純な例として記述しようとしているメソッドを考え、メソッド自体に呼び出しを追加し、必要なときに追加することです。終了します。学ぶための最良の方法は、すべてのもののように練習することです。
これはプログラマのウェブサイトなので、コードを書くことは控えますが、ここにリンクがあります
冗談を言うと、再帰の意味がわかります。
再帰は、プログラマーが自身で関数呼び出しを呼び出すために使用できるツールです。フィボナッチ数列は、再帰の使用方法の教科書の例です。
すべてではないにしても、ほとんどの再帰コードは反復関数として表現できますが、通常は乱雑です。他の再帰プログラムの良い例は、ツリー、バイナリ検索ツリー、クイックソートなどのデータ構造です。
再帰は、コードをだらしないようにするために使用されます。通常は低速で、より多くのメモリが必要です。
私はこれを使うのが好きです:
店の入り口にいる場合は、単に通り抜けてください。それ以外の場合は、一歩を踏み出し、残りの道を店まで歩いてください。
次の3つの側面を含めることが重要です。
実際、再帰は日常生活で頻繁に使用されます。そんな風に考えていないだけです。
for
ループを無意味な再帰関数に変換するのと同じだと思います。
ジョシュ・Kはすでにマトロシカ人形について言及しました。最短の人形だけが知っている何かを学びたいと仮定します。問題は、彼女が元々左の最初の写真にある背の高い人形の中に住んでいるので、彼女と直接話すことができないということです。この構造は、最も高い人形のみで終わるまで、人形はより高い人形の中に住んでいます。
だから、あなたができる唯一のことは、最も高い人形に質問することです。一番高い人形(答えが分からない)は、短い人形(最初の写真は彼女の右側)に質問を渡す必要があります。彼女にも答えがないので、次の短い人形を尋ねる必要があります。メッセージが最短の人形に届くまで、これはそのようになります。一番短い人形(秘密の答えを知っている唯一の人形)は、その答えを次のより高い人形(彼女の左側にある)に渡し、それは次のより高い人形に渡します...これは答えまで続きます一番高い人形である最終目的地に到達し、最終的に...あなた:)
これが再帰です。関数/メソッドは、予想される答えが得られるまで自分自身を呼び出します。そのため、再帰的なコードを記述するとき、再帰をいつ終了するかを決定することが非常に重要です。
最良の説明ではありませんが、うまくいけば役立つでしょう。
再帰 n。-操作がそれ自体に関して定義されるアルゴリズム設計のパターン。
典型的な例は、数値の階乗n!を見つけることです。0!= 1、および他の自然数Nの場合、Nの階乗はN以下のすべての自然数の積です。したがって、6!= 6 * 5 * 4 * 3 * 2 * 1 =720。この基本的な定義により、単純な反復ソリューションを作成できます。
int Fact(int degree)
{
int result = 1;
for(int i=degree; i>1; i--)
result *= i;
return result;
}
ただし、操作をもう一度調べてください。6!= 6 * 5 * 4 * 3 * 2 * 1。同じ定義で、5!= 5 * 4 * 3 * 2 * 1は、6と言うことができることを意味します!= 6 *(5!)。順番に、5!= 5 *(4!)など。これにより、以前のすべての操作の結果に対して実行される操作に問題を減らすことができます。これは最終的に、結果が定義によって既知であるベースケースと呼ばれるポイントまで減少します。この場合、0!= 1(ほとんどの場合、1!= 1と言うこともできます)。コンピューティングでは、メソッドを呼び出してより小さな入力を渡すことで、非常によく似た方法でアルゴリズムを定義できることがよくあります。これにより、ベースケースへの多くの再帰によって問題が軽減されます。
int Fact(int degree)
{
if(degree==0) return 1; //the base case; 0! = 1 by definition
else return degree * Fact(degree -1); //the recursive case; N! = N*(N-1)!
}
これは、多くの言語で、三項演算子を使用してさらに簡略化できます(演算子を提供しない言語ではIif関数と見なされることもあります)。
int Fact(int degree)
{
//reads equivalently to the above, but is concise and often optimizable
return degree==0 ? 1: degree * Fact(degree -1);
}
利点:
短所:
私が使用している例は、実際の生活で直面した問題です。コンテナ(旅行に使用する大型のバックパックなど)があり、総重量を知りたい場合。コンテナには2つまたは3つのゆるいアイテムがあり、他のコンテナ(たとえば、詰め物袋)があります。コンテナ全体の重量は、空のコンテナの重量にその中のすべての重量を加えたものです。ゆるいアイテムの場合は、それらの重量を量ることができます。また、スタッフサックの場合は、重量を量ることができます。または、「各サックの重量は、空のコンテナの重量とその中のすべての重量です」と言うことができます。そして、コンテナの中にゆるいアイテムがあるところに到達するまで、コンテナに入ってコンテナに入れ続けます。それは再帰です。
現実には決して起こらないと思うかもしれませんが、特定の会社や部門の人を数えたり、給与を足し合わせたりすることを想像してみてください。部門には部門などがあります。または、地域のある国での販売、一部の地域にはサブ地域などがあります。この種の問題は、ビジネスで常に発生します。
再帰は、多くのカウントの問題を解決するために使用できます。たとえば、パーティにn人(n> 1)のグループがあり、全員が他の人の手を1回だけ振るとします。ハンドシェイクは何回行われますか?解はC(n、2)= n(n-1)/ 2であることがわかりますが、次のように再帰的に解くことができます。
2人だけだとします。(検査による)答えは明らかに1です。
3人の人がいるとします。1人を選び出し、他の2人と握手することに注意してください。その後、他の2人の握手だけを数える必要があります。すでにそれを行ったので、1です。したがって、答えは2 + 1 = 3です。
n人の人がいるとします。前と同じロジックに従って、(n-1)+(n-1人の間のハンドシェイクの数)です。展開すると、(n-1)+(n-2)+ ... + 1が得られます。
再帰関数として表現され、
f(2)= 1
f(n)= n-1 + f(n-1)、n> 2
人生では(コンピュータープログラムとは対照的に)再帰は、私たちの直接の制御下で起こることはめったにありません。また、知覚は機能的に純粋であるというよりも副作用に関する傾向があるため、再帰が発生している場合は気付かないかもしれません。
しかし、ここでは世界中で再帰が発生します。たくさん。
良い例は、水循環(の簡略版)です。
これは、自己が再び発生するサイクルです。再帰的です。
再帰を取得できる別の場所は、英語(および一般に人間の言語)です。最初は気付かないかもしれませんが、同じシンボルの別のインスタンスの内側にシンボルの1つのインスタンスを埋め込むことができるため、文を生成する方法は再帰的です。
Steven PinkerのThe Language Instinctから:
女の子がアイスクリームを食べるか、女の子がキャンディーを食べると、男の子はホットドッグを食べる
それは他の文全体を含む文全体です:
女の子はアイスクリームを食べる
女の子はお菓子を食べる
少年はホットドッグを食べる
完全な文を理解する行為には、小さな文を理解することが含まれます。これは、同じ文を完全な文として理解するために同じ精神的なトリックを使用します。
プログラミングの観点から再帰を理解するには、再帰で解決できる問題を見て、それがなぜあるべきか、それが何をする必要があるのかを理解するのが最も簡単です。
この例では、最大公約数関数、または略してgcdを使用します。
あなたの2つの数字a
とb
。それらのgcd(どちらも0でないと仮定)を見つけるには、a
がに均等に割り切れるかどうかを確認する必要がありますb
。それがb
gcdである場合、そうでない場合はgcd b
との残りを確認する必要がありますa/b
。
gcd関数がgcd関数を呼び出しているため、これが再帰関数であることを既に確認できているはずです。ただ家に帰るために、ここではc#にあります(ここでも、0はパラメーターとして渡されないことを前提としています)。
int gcd(int a, int b)
{
if (a % b == 0) //this is a stopping condition
{
return b;
}
return (gcd(b, a % b)); //the call to gcd here makes this function recursive
}
プログラムでは、停止条件を設定することが重要です。停止条件がないと、関数が永久に繰り返され、最終的にスタックオーバーフローが発生します。
ここでwhileループやその他の反復構造ではなく再帰を使用する理由は、コードを読むと、何をしていて次に何が起こるかを伝えるため、正しく動作しているかどうかを把握しやすくなるためです。
再帰の実際の例を次に示します。
彼らに漫画のコレクションがあり、それをすべて大きな山に混ぜることを想像してください。注意-実際にコレクションを持っている場合、そのアイデアについて言及しただけで、すぐにあなたを殺してしまうかもしれません。
次に、このマニュアルの助けを借りて、この大きな未分類の漫画の山を並べ替えます:
Manual: How to sort a pile of comics
Check the pile if it is already sorted. If it is, then done.
As long as there are comics in the pile, put each one on another pile,
ordered from left to right in ascending order:
If your current pile contains different comics, pile them by comic.
If not and your current pile contains different years, pile them by year.
If not and your current pile contains different tenth digits, pile them
by this digit: Issue 1 to 9, 10 to 19, and so on.
If not then "pile" them by issue number.
Refer to the "Manual: How to sort a pile of comics" to separately sort each
of the new piles.
Collect the piles back to a big pile from left to right.
Done.
ここでの良い点は、それらが単一の問題になっているとき、彼らは地面に見える前にローカルパイルが見える完全な「スタックフレーム」を持っていることです。マニュアルの複数のプリントアウトを提供し、現在のレベル(ローカル変数の状態)にあるマークが付いた各パイルレベルを脇に置いて、各Doneでそこから続行できるようにします。
基本的に再帰とは、まさに同じプロセスを実行することです。より詳細なレベルで実行すればするほど、それが実行されます。
再帰の素晴らしい説明は、文字通り「自身の中から再発するアクション」です。
画家が壁をペイントすることを考えてみましょう。アクションは「少し右にスクートするよりも天井から床にストリップをペイントし、少し右にスクートするよりも天井から床にストリップをペイントし、(少し右にスクートし、(など)))」よりも天井から床までストリップします。
彼のpaint()関数は、自分自身を何度も繰り返し呼び出して、より大きなpaint_wall()関数を作成します。
うまくいけば、この貧しい画家には何らかの停止条件があります:)