再帰の概念は現実の世界ではあまり一般的ではありません。だから、初心者プログラマーにとっては少し混乱しているようです。でも、だんだんコンセプトに慣れてきていると思います。では、アイデアを簡単に把握するための良い説明は何でしょうか?
同じ関数内からの関数の呼び出し。
それを使用する方法、いつ使用するか、悪いデザインを回避する方法を知ることは重要であり、自分で試して何が起こるかを理解する必要があります。
あなたが知る必要がある最も重要なことは、決して終わらないループを取得しないように非常に注意することです。 pramodc84からの回答 質問への回答には、この欠点があります。
再帰関数は常に条件をチェックして、自分自身を再度呼び出す必要があるかどうかを判断する必要があります。
再帰を使用する最も古典的な例は、深さに静的な制限がないツリーで作業することです。これは、再帰を使用する必要があるタスクです。
再帰的プログラミングとは、問題を徐々に減らして、自分自身のバージョンを簡単に解決できるようにするプロセスです。
すべての再帰関数は次の傾向があります。
ステップ2が3の前にあり、ステップ4が取るに足らない(連結、合計、または何もない)場合、これにより tail recursion が有効になります。現在の手順を完了するには、問題のサブドメインからの結果が必要になる場合があるため、手順2は手順3の後に来ることがよくあります。
単純な二分木のトラバーサルを取ります。トラバーサルは、必要なものに応じて、プレオーダー、インオーダー、ポストオーダーで行うことができます。
B
A C
予約注文:B A C
traverse(tree):
visit the node
traverse(left)
traverse(right)
順番:A B C
traverse(tree):
traverse(left)
visit the node
traverse(right)
ポストオーダー:A C B
traverse(tree):
traverse(left)
traverse(right)
visit the node
非常に多くの再帰的な問題は map 演算または fold の特定のケースです-これら2つの演算だけを理解すると、再帰の適切なユースケースを大幅に理解できます。
OPは、再帰は現実の世界には存在しないと述べましたが、私は違いますようにお願いします。
ピザを切り分ける実際の「操作」を見てみましょう。オーブンからピザを取り出し、それを提供するために、半分にカットし、次に半分を半分にカットしてから、結果として生じる半分を再び半分にカットする必要があります。
あなたが望む結果(スライスの数)が得られるまで、あなたが何度も何度も行っているピザをカットする操作。そして議論のために、カットされていないピザ自体がスライスであるとしましょう。
Rubyの例を次に示します。
def cut_pizza(existing_slices、desired_slices) if existing_slices!= desired_slices #まだ十分なスライスがないため、みんなに食べさせることができないため、 #ピザのスライスを切り、その数を2倍にします new_slices = existing_slices * 2 #これが再帰呼び出しです cut_pizza(new_slices、desired_slices) else #必要なスライス数があるため、再帰を続けるのではなく、ここに #を返します return existing_slices end end ピザ= 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つのループのみを処理する単純な例として考え、メソッド自体への呼び出しを追加し、必要に応じて追加することです。終了します。学ぶための最良の方法は、すべてのもののように練習することです。
これはプログラマーのWebサイトなので、コードの記述は控えますが、ここでは良い link です。
そのジョークを手に入れれば、再帰の意味がわかります。
再帰は、プログラマがそれ自体で関数呼び出しを呼び出すために使用できるツールです。フィボナッチ数列は、再帰がどのように使用されるかの教科書の例です。
すべてではないにしても、ほとんどの再帰的コードは反復関数として表現できますが、通常は面倒です。他の再帰プログラムの良い例は、ツリー、二分探索木、さらにはクイックソートなどのデータ構造です。
再帰は、コードのざらつきを少なくするために使用されます。通常、コードの速度が遅く、より多くのメモリを必要とします。
私はこれを使いたいです:
あなたが店の入り口にいるなら、単にそれを通り抜けてください。それ以外の場合は、1つのステップを実行してから、残りの店まで歩きます。
次の3つの側面を含めることが重要です。
私たちは実際に日常生活で再帰を頻繁に使用しています。私たちはそのように考えていません。
私があなたに指摘する最良の例は、K&RによるCプログラミング言語です。その本(および私はメモリから引用しています)では、再帰(単独)のインデックスページのエントリは、再帰について話している実際のページと、インデックスページも同様です。
Josh Kはすでに Matroshka 人形について言及しました。最短の人形だけが知っていることを学びたいと仮定します。問題は、彼女がもともと 内側 に住んでいるため、直接彼女と直接話をすることができないことです。 。この構造は、高さのある人形だけで終わるまで、そのように続きます(人形は背の高い人形の中に住んでいます)。
だからあなたができる唯一のことは、一番高い人形に質問をすることです。一番高い人形(答えがわからない)は、質問を短い人形(最初の写真では彼女の右側にあります)に渡す必要があります。彼女にも答えがないので、次の短い人形に尋ねる必要があります。これは、メッセージが最短の人形に到達するまでそのようになります。最短の人形(秘密の答えを知っている唯一の人形)が次の背の高い人形(彼女の左側にあります)に答えを渡し、次の背の高い人形に渡します...これは答えまで続きます最も高い人形である最終目的地に到達し、最後に...あなた:)
これは、再帰が実際に行うことです。関数/メソッドは、期待される答えが得られるまで自分自身を呼び出します。そのため、再帰的なコードを作成するときは、再帰をいつ終了するかを決定することが非常に重要です。
最良の説明ではありませんが、うまくいけば役に立ちます。
再帰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)!
}
これは、多くの言語で、3項演算子を使用してさらに簡略化できます(このような演算子を提供しない言語では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
これは再帰の実際の例です。
彼らが漫画のコレクションを持っていると想像してみましょう。あなたはそれを大きな山に混ぜるつもりです。注意してください-彼らが本当にコレクションを持っている場合、あなたがそうするという考えに言及するだけで、彼らはあなたを即座に殺すかもしれません。
次に、このマニュアルを使用して、この並べ替えられていないコミックの大きな山を並べ替えます。
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.
ここでの良い点は、単一の問題が発生すると、ローカルパイルが地面に現れる前に完全な「スタックフレーム」が表示されることです。それらにマニュアルの複数の印刷物を渡して、現在のレベルにいるマーク(つまり、ローカル変数の状態)を付けて各パイルレベルを1つ脇に置きます。これにより、各Doneで続行できます。
それが再帰の基本です。まったく同じプロセスを実行します。詳細レベルが高ければ高いほど、そのプロセスに進んでいきます。
実生活では(コンピュータプログラムではなく)、再帰が発生するのが混乱する可能性があるため、直接制御下ではめったに発生しません。また、知覚は機能的に純粋ではなく、副作用についての傾向があるため、再帰が発生している場合は気付かない場合があります。
ただし、この世界では再帰が発生します。たくさん。
良い例は水循環(の簡略版)です:
これは、自分自身を再び発生させるサイクルです。再帰的です。
再帰を取得できるもう1つの場所は、英語(および一般的には人間の言語)です。最初は認識できないかもしれませんが、文を生成する方法は再帰的です。これは、ルールによって、あるシンボルのインスタンスを同じシンボルの別のインスタンスの横に埋め込むことができるためです。
スティーブンピンカーの言語本能から:
女の子がアイスクリームを食べるか女の子がキャンディーを食べるなら、男の子はホットドッグを食べる
それは他の全文を含む全文です:
女の子はアイスクリームを食べる
女の子はお菓子を食べる
少年はホットドッグを食べる
全文を理解する行為には、より短い文を理解することが含まれます。これらの文は、全文と同じ精神的トリックを理解するために使用されます。
プログラミングの観点から再帰を理解するには、再帰で解決できる問題を調べ、それがなぜあるべきか、そしてそれが何をすべきかを理解するのが最も簡単です。
この例では、最大公約数関数、または略してgcdを使用します。
2つの数字a
とb
があります。それらのgcdを見つけるには(どちらも0でないと仮定)、a
がb
に均等に割り切れるかどうかを確認する必要があります。場合は、b
はgcdです。それ以外の場合は、b
のgcdと残りの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ループやその他の反復構造ではなく再帰を使用する理由は、コードを読むと、何が行われていて次に何が起こるかがわかるため、正しく機能しているかどうかを簡単に理解できるためです。 。