平易な英語では、再帰とは何ですか?


74

再帰の考え方は、現実の世界ではあまり一般的ではありません。そのため、初心者のプログラマにとっては少し混乱するようです。とはいえ、彼らは徐々にコンセプトに慣れてくると思います。だから、彼らがアイデアを簡単に把握するための良い説明は何ですか?




1
再帰は、関数がそれ自体を呼び出すことができる場合です。「名前空間とスコープ、およびパラメーターが関数に渡される方法を完全に理解している場合、再帰はすでにわかっています。例を示しますが、それらが自分でどのように機能するかを理解できるはずです。」生徒は一般的に再帰に苦労します。それは混乱を招くからではなく、変数のスコープ/名前空間をしっかり把握していないためです。再帰に飛び込む前に、異なるスコープの変数に同じ名前を意図的に与えて混乱させているプログラムを生徒が適切にトレースできることを確認してください。
dspyz


1
再帰を理解するには、まず再帰を理解する必要があります
ゴアマン

回答:


110

再帰を説明するために、私は異なる説明の組み合わせを使用します。

  • コンセプトを説明し、
  • それが重要な理由を説明し、
  • 入手方法を説明します。

まず第一に、Wolfram | AlphaWikipediaよりも簡単な用語で定義しています。

特定の数学的操作を繰り返すことにより各用語が生成されるような式。


数学

あなたの学生(または私は学生を言うよにあなたが今から、あまりにも説明者)が、少なくともいくつかの数学的背景を持っている場合、彼らは明らかに、すでにシリーズとの彼らの概念研究することによって、再帰が発生しました再帰性とその再発の関係を

開始するための非常に良い方法は、シリーズでデモンストレーションし、それが非常に単純に再帰についてであると言うことです:

  • 数学関数...
  • ... n番目の要素に対応する値を計算するために自分自身を呼び出します...
  • ...そして、いくつかの境界を定義します。

通常、あなたはせいぜい「ハァッ、ホェーテブ」を使用しますが、それは彼らがまだそれを使用していないためです。


コーディング例

残りの部分については、実際には、ポインターに関して指摘し質問に対する私の回答補遺で示したものの詳細なバージョンです(悪戯)。

この段階では、生徒は通常、画面に何かを印刷する方法を知っています。Cを使用していると仮定すると、彼らはwriteor を使用して単一の文字を印刷する方法を知っていますprintf。また、制御ループについても知っています。

私は通常、いくつかの反復的で単純なプログラミングの問題を解決するまで頼ります。

  • 階乗操作、
  • アルファベットプリンター、
  • 逆アルファベットプリンター、
  • exponentationの操作。

階乗

階乗は理解するための非常に単純な数学概念であり、実装はその数学表現に非常に近いものです。ただし、最初は取得できない場合があります。

階乗の再帰的定義

アルファベット

アルファベット版は、再帰文の順序について考えるように教えるのに興味深いです。ポインタのように、それらはあなたにランダムに線を投げます。ポイントは、ループが条件を修正することのいずれかによって反転することができます実現にそれらをもたらすことですOR justあなたの関数内の文の順序を反転させて。それは彼らにとって視覚的なものであるため、アルファベットの印刷が役立ちます。呼び出しごとに1文字を出力し、それ自体を再帰的に呼び出して次の(または前の)文字を書き込む関数を作成するだけです。

FPファン、出力ストリームに印刷することは今のところ副作用であるという事実をスキップしてください... FPの面倒なことはやめましょう。(ただし、リストをサポートする言語を使用する場合は、各反復でリストに連結し、最終結果を出力してください。通常、Cで開始します。残念ながら、この種の問題や概念には最適ではありません) 。

べき乗

べき乗の問題は、やや難しくなります(学習のこの段階では)。明らかに、概念は階乗の場合とまったく同じであり、複数のパラメーターがあることを除いて、追加の複雑さはありません... そして、それは通常、人々を混乱させ、最初に彼らを捨てるのに十分です。

その単純な形式:

べき乗演算の単純な形式

繰り返しによってこのように表現できます:

べき乗演算の再帰関係

もっと強く

これらの簡単な問題がチュートリアルで示され、再実装されたら、もう少し難しい(しかし非常に古典的な)演習を行うことができます。

注:繰り返しますが、これらのいくつかは実際にはそれほど難しくありません...まったく同じ角度またはわずかに異なる角度から問題にアプローチします。しかし、練習すれば完璧になります。


ヘルパー

参照

一部の読書は決して痛くない。まあそれは最初はそうなるでしょう、そして彼らはさらに失われたと感じるでしょう。それはあなたの上で成長し、ある日あなたが最終的にそれを手に入れることに気付くまであなたの頭の後ろに座っているようなものです。そして、あなたはあなたが読んだこれらのものを振り返ります。現時点では、Wikipediaの再帰、コンピューターサイエンス再帰、 および再帰関係のページで対応します。

レベル/深さ

学生にコーディング経験があまりないと仮定して、コードスタブを提供します。最初の試行の後、再帰レベルを表示できる印刷機能を提供します。レベルの数値を印刷すると役立ちます。

引き出しとしてのスタック図

印刷結果(またはレベルの出力)をインデントすることも役立ちます。プログラムが実行していることの別の視覚的表現を提供し、引き出しやファイルシステムエクスプローラーのフォルダーなどのスタックコンテキストを開閉します。

再帰的な頭字語

学生が既にコンピューターカルチャーに精通している場合、再帰的な頭字語を使用した名前のプロジェクト/ソフトウェアを既に使用している可能性があります。それはしばらくの間、特にGNUプロジェクトで行われている伝統です。以下に例を示します。

再帰的:

  • GNU-「GNUはUnixではない」
  • ナギオス-「ナギオスはセイントフッドを主張しない」
  • PHP-「PHP Hypertext Preprocessor」(およびオリジナルの「Personal Home Page」)
  • ワイン-「ワインはエミュレータではありません」
  • Zile-「Zile Is Lossy Emacs」

相互再帰的:

  • HURD-「Unix置換デーモンのHIRD」(HIRDは「深度を表すインターフェイスのHURD」)

彼らに彼ら自身のものを考え出させてください。

同様に、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 NorvigSolving Every Sudoku Puzzleを参照してください)。


5
ほぼ5回目または6回目に答えを読んだ後、あなたの答えを読んでいます。ここは他のユーザーを惹きつけるには長すぎたと思います。ここで再帰を教えることについて多くのことを学びました。教師として、あなたはrecursion-教えるために私の考えを評価してくださいだろうprogrammers.stackexchange.com/questions/25052/...を
Gulshanの

9
@Gulshan、この答えは、これから行われるものとほぼ同じくらい包括的で、カジュアルな読者には簡単に「スキミング」されると思います。したがって、それはstatic unsigned int vote = 1;私から得ます。静的なユーモアを許してください:)これはこれまでのベストアンサーです。
ティムポスト

1
@Gulsan:学びたいと思う人だけが時間をかけることをいとわないのは適切です:)私は本当に気にしません。簡潔な答えがエレガントであり、一般的な概念を開始したり説明したりするのに役立つ多くの有用な情報を伝えることがあります。私はその答えをもっと長くしたかったのですが、OPが「正しい」答えを与えられた質問に言及し、同様の質問をすることを考慮して、同じ種類の答えを出すことが適切だと考えました。何か学んだことを嬉しく思います。
ヘイレム

@Gulshan:今、あなたの答えに関して:最初のポイントは私をたくさん混乱させました、私は言わなければなりません。再帰関数の概念を、時間の経過とともに状態が徐々に変化するものとして説明するのが好きですが、あなたの表現方法は少し奇妙だと思います。あなたの3点を読んだ後、多くの学生が突然ハノイを解くことができるとは思わないでしょう。しかし、それは単なる言葉遣いの問題かもしれません。
ヘイレム

同じ変数が異なる再帰の深さで異なる値を持つことができることを示す良い方法に出くわしました:いくつかの再帰的なコードに従う際に、生徒に自分が行っているステップと変数の値を書き留めてもらうことを検討してください。彼らが再帰に達したとき、彼らは新しいピースで再び始めてください。終了条件に達したら、前のピースに戻ります。これは本質的に呼び出しスタックをエミュレートしますが、これらの違いを示す良い方法です。
アンディハント

58

同じ関数内からの関数の呼び出し。


2
これは、実際に始めるのに最適な説明です。シンプルで要点; この概要を確立したら、すべてのヘッドトリップの詳細に進みます。
11

27

再帰は、それ自体を呼び出す関数です。

それを使用する方法、使用するタイミング、悪いデザインを避ける方法は知っておくことが重要です。これはあなた自身でそれを試して、何が起こるかを理解する必要があります。

知っておく必要がある最も重要なことは、決して終わらないループを取得しないように非常に注意することです。pramodc84からの質問への答えには、次のような欠点があります。終わらない...
再帰関数は、常に自分自身を呼び出す必要があるかどうかを判断するための条件をチェックする必要があります。

再帰を使用する最も典型的な例は、静的な深さ制限のないツリーで作業することです。これは、再帰を使用する必要があるタスクです。


最後の段落を反復的に実装することもできますが、再帰的には明らかに改善されます。「それをどのように使用するか、いつ使用するか、悪いデザインを避ける方法は知っておくことが重要です。それはあなた自身でそれを試して、何が起こるかを理解する必要があります。」そのようなこと。
ヘイレム

@haylem:「使用方法、使用時期、デザインの悪さを回避する方法」に対する答えは正しいです。OPが求めるものは、「自分で試してみるだけでなく」 「私が言ったように)、しかし、それはここでの質問への迅速な答えではなく、より多くの教育の種類の広範な講義を必要とします。あなたはあなたの答えで非常に良い仕事をしました。そのために+1 ...コンセプトをよりよく理解する必要がある人は、あなたの答えを読むことで恩恵を受けるでしょう。

お互いを呼び出す関数のペアはどうでしょう。AはBを呼び出します。Bは、何らかの条件に達するまでAを再度呼び出します。これはまだ再帰と見なされますか?
サンティアゴスキー

はい、関数はaまだ間接的に(を呼び出すことによりb)自身を呼び出します。
やさしい

1
@santiagozky:kindallが言ったように、それはまだ再帰です。ただし、それをしないことをお勧めします。再帰を使用する場合、コード内で再帰が設定されていることが非常に明確になります。別の関数を介して間接的に自分自身を呼び出すと、何が起こっているのかを見るのがはるかに難しくなります。関数がそれ自体を効果的に呼び出すことを知らない場合、あなた(またはこの関数を作成しなかった他の人)が(コードの機能を変更しながら)再帰の条件を破って終了することができます。終わりのないループでデッドロックに陥ります。

21

再帰プログラミングは、問題を段階的に減らして、それ自体のバージョンを解決しやすくするプロセスです。

すべての再帰関数は次の傾向があります。

  1. 処理するリスト、その他の構造、または問題のあるドメインを取得する
  2. 現在のポイント/ステップに対処する
  3. 残り/サブドメインで自分自身を呼び出す
  4. サブドメイン作業の結果を結合または使用する

ステップ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つの操作だけを理解すると、再帰の適切なユースケースを十分に理解できます。


実用的な再帰の重要な要素は、わずかに小さな問題の解決策を使用して大きな問題を解決するという考え方です。それ以外の場合は、無限の再帰があります。
バリーブラウン

@Barry Brown:まったくその通りです。したがって、「...問題を軽減して、より簡単に解決できるバージョンに」という
Orbling

必ずしもそうとは限りません...特に問題を分割して克服する場合、または単純なケースに帰着する再帰関係を本当に定義する状況では、そうです。しかし、問題の反復Nごとに、計算可能なケースN + 1があることを証明することの方が重要だと思います。
haylem

1
@Sean McMillan:Recursionは、それに適したドメインで使用すると強力なツールです。多くの場合、比較的些細な問題を処理するための賢い方法として使用され、手元のタスクの本質を非常に難読化していると思います。
11

20

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個のスライスを要求した場合、永遠に実行されないように変更できますか?


16

さて、このシンプルで簡潔なものにしようと思っています。

再帰関数は、それ自体を呼び出す関数です。再帰関数は次の3つの要素で構成されます。

  1. 論理
  2. 自身への呼び出し
  3. いつ終了するか。

再帰的なメソッドを記述する最良の方法は、反復しようとするプロセスのループを1つだけ処理する単純な例として記述しようとしているメソッドを考え、メソッド自体に呼び出しを追加し、必要なときに追加することです。終了します。学ぶための最良の方法は、すべてのもののように練習することです。

これはプログラマのウェブサイトなので、コードを書くことは控えますが、ここにリンクがあります

冗談を言うと、再帰の意味がわかります。


この回答も参照してください:Programmers.stackexchange.com/questions/25052/…(-:
Murph

4
厳密には、再帰は終了条件を必要としません。再帰が終了することを保証すると、再帰関数が解決できる問題の種類が制限され、終了をまったく必要としないセマンティックプレゼンテーションがいくつかあります。
ドナルドフェローズ

6

再帰は、プログラマーが自身で関数呼び出しを呼び出すために使用できるツールです。フィボナッチ数列は、再帰の使用方法の教科書の例です。

すべてではないにしても、ほとんどの再帰コードは反復関数として表現できますが、通常は乱雑です。他の再帰プログラムの良い例は、ツリー、バイナリ検索ツリー、クイックソートなどのデータ構造です。

再帰は、コードをだらしないようにするために使用されます。通常は低速で、より多くのメモリが必要です。


それが遅いか、より多くのメモリを必要とするかは、手元の使用に大きく依存します。
Orbling

5
フィボナッチ数列の計算は、再帰的に行うのは恐ろしいことです。ツリートラバーサルは、再帰のはるかに自然な使用法です。通常、再帰が適切に使用されている場合、呼び出しスタックの代わりに独自のスタックを維持する必要があるため、低速ではなく、より多くのメモリを必要としません。
デビッドソーンリー

1
@デイブ:私はそれについて異議を唱えないでしょうが、フィボナッチは最初から良い例だと思います。
ブライアンハリントン

5

私はこれを使うのが好きです:

店まではどうやって歩きますか?

店の入り口にいる場合は、単に通り抜けてください。それ以外の場合は、一歩を踏み出し、残りの道を店まで歩いてください。

次の3つの側面を含めることが重要です。

  • 些細なベースケース
  • 問題の小さな部分を解決する
  • 問題の残りを再帰的に解決する

実際、再帰は日常生活で頻繁に使用されます。そんな風に考えていないだけです。


THatは再帰ではありません。それを2つに分割すると、店までの道の半分を歩き、残りの半分を歩きます。再帰。

2
これは再帰です。これは分割して征服するものではありませんが、再帰の一種です。グラフアルゴリズム(パス検出など)は再帰的な概念に満ちています。
-deadalnix

2
これは再帰ですが、「一歩踏み込んで残りの部分をストアに移動する」ことを反復アルゴリズムに精神的に変換するのはあまりにも簡単すぎるため、貧弱な例だと思います。これは、きちんと書かれたforループを無意味な再帰関数に変換するのと同じだと思います。
ブライアン

3

私が指摘する最良の例は、K&RのCプログラミング言語です。その本(およびメモリから引用しています)では、再帰(単独)のインデックスページのエントリは、再帰と、インデックスページも同様です。


2

ジョシュ・Kはすでにマトロシカ人形について言及しました。最短の人形だけが知っている何かを学びたいと仮定します。問題は、彼女が元々左の最初の写真にある背の高い人形のに住んでいるので、彼女と直接話すことができないということです。この構造は、最も高い人形のみで終わるまで、人形はより高い人形の中に住んでいます。

だから、あなたができる唯一のことは、最も高い人形に質問することです。一番高い人形(答えが分からない)は、短い人形(最初の写真は彼女の右側)に質問を渡す必要があります。彼女にも答えがないので、次の短い人形を尋ねる必要があります。メッセージが最短の人形に届くまで、これはそのようになります。一番短い人形(秘密の答えを知っている唯一の人形)は、その答えを次のより高い人形(彼女の左側にある)に渡し、それは次のより高い人形に渡します...これは答えまで続きます一番高い人形である最終目的地に到達し、最終的に...あなた:)

これが再帰です。関数/メソッドは、予想される答えが得られるまで自分自身を呼び出します。そのため、再帰的なコードを記述するとき、再帰をいつ終了するかを決定することが非常に重要です。

最良の説明ではありませんが、うまくいけば役立つでしょう。


2

再帰 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);
}

利点:

  • 自然な表現-多くの種類のアルゴリズムでは、これは関数を表現する非常に自然な方法です。
  • LOCの削減-関数を再帰的に定義する方がはるかに簡潔です。
  • 速度-特定の場合、言語とコンピューターアーキテクチャに応じて、アルゴリズムの再帰は同等の反復ソリューションよりも高速です。これは、通常、関数呼び出しを行うことは、反復的にループするために必要な操作とメモリアクセスよりもハードウェアレベルでの高速操作であるためです。
  • 分割可能性-多くの再帰アルゴリズムは、「分割して征服する」という考え方です。操作の結果は、入力の2つの半分のそれぞれに対して実行された同じ操作の結果の関数です。これにより、各レベルで作業を2つに分割でき、可能であれば、残りの半分を別の「実行ユニット」に渡して処理できます。これは通常、反復アルゴリズムでは困難または不可能です。

短所:

  • 理解が必要-何が起こっているのかを理解するために、再帰の概念を「把握」して、効果的な再帰アルゴリズムを作成して維持する必要があります。それ以外の場合は、単に黒魔術のように見えます。
  • コンテキスト依存-再帰が良いアイデアであるかどうかは、アルゴリズムがそれ自体に関してどれほどエレガントに定義できるかに依存します。たとえば、再帰的なSelectionSortを構築することは可能ですが、通常、反復アルゴリズムの方が理解しやすいです。
  • 呼び出しスタックのRAMアクセスと引き換え-通常、関数呼び出しはキャッシュアクセスよりも安価であり、反復よりも再帰を高速化できます。ただし、通常、呼び出しスタックの深さには制限があり、反復アルゴリズムが機能する場所で再帰エラーが発生する可能性があります。
  • 無限再帰-いつ停止するかを知る必要があります。無限の反復も可能ですが、通常、含まれるループ構造は理解しやすく、したがってデバッグが容易です。

1

私が使用している例は、実際の生活で直面した問題です。コンテナ(旅行に使用する大型のバックパックなど)があり、総重量を知りたい場合。コンテナには2つまたは3つのゆるいアイテムがあり、他のコンテナ(たとえば、詰め物袋)があります。コンテナ全体の重量は、空のコンテナの重量にその中のすべての重量を加えたものです。ゆるいアイテムの場合は、それらの重量を量ることができます。また、スタッフサックの場合は、重量を量ることができます。または、「各サックの重量は、空のコンテナの重量とその中のすべての重量です」と言うことができます。そして、コンテナの中にゆるいアイテムがあるところに到達するまで、コンテナに入ってコンテナに入れ続けます。それは再帰です。

現実には決して起こらないと思うかもしれませんが、特定の会社や部門の人を数えたり、給与を足し合わせたりすることを想像してみてください。部門には部門などがあります。または、地域のある国での販売、一部の地域にはサブ地域などがあります。この種の問題は、ビジネスで常に発生します。


0

再帰は、多くのカウントの問題を解決するために使用できます。たとえば、パーティに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


0

人生では(コンピュータープログラムとは対照的に)再帰は、私たちの直接の制御下で起こることはめったにありません。また、知覚は機能的に純粋であるというよりも副作用に関する傾向があるため、再帰が発生している場合は気付かないかもしれません。

しかし、ここでは世界中で再帰が発生します。たくさん。

良い例は、水循環(の簡略版)です。

  • 太陽が湖を温める
  • 水は空に上がり、雲を形成します
  • 雲が山に漂います
  • 山では、空気が冷たすぎて湿気を保持できません
  • 雨が降る
  • 川が形成される
  • 川の水は湖に流れ込む

これは、自己が再び発生するサイクルです。再帰的です。

再帰を取得できる別の場所は、英語(および一般に人間の言語)です。最初は気付かないかもしれませんが、同じシンボルの別のインスタンスの内側にシンボルの1つのインスタンスを埋め込むことができるため、文を生成する方法は再帰的です。

Steven PinkerのThe Language Instinctから:

女の子がアイスクリームを食べるか、女の子がキャンディーを食べると、男の子はホットドッグを食べる

それは他の文全体を含む文全体です:

女の子はアイスクリームを食べる

女の子はお菓子を食べる

少年はホットドッグを食べる

完全な文を理解する行為には、小さな文を理解することが含まれます。これは、同じ文を完全な文として理解するために同じ精神的なトリックを使用します。

プログラミングの観点から再帰を理解するには、再帰で解決できる問題を見て、それがなぜあるべきか、それが何をする必要があるのか​​を理解するのが最も簡単です。

この例では、最大公約数関数、または略してgcdを使用します。

あなたの2つの数字ab。それらのgcd(どちらも0でないと仮定)を見つけるには、aがに均等に割り切れるかどうかを確認する必要がありますb。それがbgcdである場合、そうでない場合は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ループやその他の反復構造ではなく再帰を使用する理由は、コードを読むと、何をしていて次に何が起こるかを伝えるため、正しく動作しているかどうかを把握しやすくなるためです。


1
水循環の例は反復的であることがわかりました。2番目の言語の例は、再帰よりも分割統治のようです。
グルシャン

@Gulshan:水循環は、再帰関数のように自己を繰り返すため、再帰的と言えます。forループのように、複数のオブジェクト(壁、天井など)に対して同じステップを実行する部屋をペイントするようなものではありません。この言語の例では、分割と征服を使用していますが、呼び出される「関数」は、ネストされた文を処理するために自己を呼び出しているため、そのように再帰的です。
マットエレン

水循環では、サイクルは太陽によって開始され、サイクルの他の要素が太陽を再び開始させることはありません。それで、再帰呼び出しはどこにありますか?
グルシャン

再帰呼び出しはありません!関数ではありません。:D自己を再帰させるため、再帰的です。湖からの水は湖に戻り、サイクルが再び始まります。他のシステムが湖に水を入れている場合、それは反復的です。
マットエレン

1
水循環はwhileループです。確かに、whileループは再帰を使用して表現できますが、そうするとスタックが破壊されます。しないでください。
ブライアン

0

再帰の実際の例を次に示します。

彼らに漫画のコレクションがあり、それをすべて大きな山に混ぜることを想像してください。注意-実際にコレクションを持っている場合、そのアイデアについて言及しただけで、すぐにあなたを殺してしまうかもしれません。

次に、このマニュアルの助けを借りて、この大きな未分類の漫画の山を並べ替えます:

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でそこから続行できるようにします。

基本的に再帰とは、まさに同じプロセスを実行することです。より詳細なレベルで実行すればするほど、それが実行されます。


-1
  • 終了条件に達したら終了する
  • 物事の状態を変えるために何かをする
  • 物事の現在の状態から始めてすべての作業を行う

再帰は、何かに到達するまで繰り返さなければならない何かを表現するための非常に簡潔な方法です。



-2

再帰の素晴らしい説明は、文字通り「自身の中から再発するアクション」です。

画家が壁をペイントすることを考えてみましょう。アクションは「少し右にスクートするよりも天井から床にストリップをペイントし、少し右にスクートするよりも天井から床にストリップをペイントし、(少し右にスクートし、(など)))」よりも天井から床までストリップします。

彼のpaint()関数は、自分自身を何度も繰り返し呼び出して、より大きなpaint_wall()関数を作成します。

うまくいけば、この貧しい画家には何らかの停止条件があります:)


6
私にとっては、この例は再帰的ではなく、反復的な手順のように見えます。
グルシャン

@Gulshan:再帰と反復は同様のことを行い、多くの場合、どちらでもうまく機能します。関数型言語は通常、反復ではなく再帰を使用します。同じことを繰り返し書くのが厄介な再帰のより良い例があります。
デビッドソーンリー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.