再帰とは何ですか?いつ使用すべきですか?


121

メーリングリストやオンラインディスカッションで定期的に出てきそうなトピックの1つは、コンピュータサイエンスの学位を取得することのメリット(またはその欠如)です。ネガティブパーティーに対して何度も繰り返し登場するように思われる議論は、彼らが何年もコーディングしていて、再帰を使用したことがないということです。

だから問題は:

  1. 再帰とは何ですか?
  2. 再帰はいつ使用しますか?
  3. なぜ人々は再帰を使用しないのですか?

9
そして多分これは役立ちます:stackoverflow.com/questions/126756/...
kennytm

3
これは概念を把握するのに役立つ可能性があります移動し、このページ上の質問の第二のコメント内のリンクにコメントを行うに言うことを実行します。stackoverflow.com/questions/3021/...
dtmland

回答:


86

このスレッドには、再帰について多くの優れた説明があります。この答えは、ほとんどの言語で再帰を使用してはいけない理由についてです。 、Ruby、Java、およびC#)の反復は、再帰よりもはるかに望ましい方法です。

理由を確認するには、上記の言語が関数の呼び出しに使用する手順を確認してください。

  1. スタックは、関数の引数とローカル変数のためにスタックに分割されます
  2. 関数の引数がこの新しいスペースにコピーされます
  3. コントロールは関数にジャンプします
  4. 関数のコードが実行されます
  5. 関数の結果が戻り値にコピーされます
  6. スタックは前の位置に巻き戻されます
  7. コントロールは、関数が呼び出された場所にジャンプして戻ります

これらの手順をすべて実行するには時間がかかります。通常、ループを繰り返すのにかかる時間よりも少し長くなります。ただし、実際の問題はステップ1にあります。多くのプログラムが起動すると、スタックに1つのメモリチャンクが割り当てられ、そのメモリが不足すると(多くの場合、再帰が原因ではない場合もある)、スタックオーバーフローが原因でプログラムがクラッシュします

そのため、これらの言語では再帰が遅くなり、クラッシュに対して脆弱になります。それを使用するためのいくつかの議論はまだあります。一般に、再帰的に記述されたコードは、読み方がわかれば、短くてエレガントになります。

言語の実装者が、スタックオーバーフローのいくつかのクラスを排除できる、呼び出された末尾呼び出しの最適化を使用できる手法があります。簡潔に言うと、関数の戻り式が単に関数呼び出しの結果である場合、スタックに新しいレベルを追加する必要はなく、呼び出されている関数に現在のレベルを再利用できます。残念ながら、いくつかの必須の言語実装には、末尾呼び出しの最適化が組み込まれています。

* 再帰が大好きです。 私のお気に入りの静的言語はループをまったく使用していません。再帰が何かを繰り返し行う唯一の方法です。再帰が調整されていない言語では、一般的に再帰は良い考えだとは思いません。

**ちなみに、マリオ、あなたのArrangeString関数の典型的な名前は "join"です。そして、あなたの選択した言語がまだそれを実装していないなら、私は驚くでしょう。


1
再帰の固有のオーバーヘッドの説明を見るのは良いことです。私もその答えに触れました。しかし、私にとって、再帰の大きな強みは、コールスタックで実行できることです。繰り返し分岐する簡潔なアルゴリズムを記述して、階層(親/子関係)のクロールなどを簡単に行うことができます。例については、私の回答を参照してください。
スティーブウォーサム

7
「再帰とは何か、いつ再帰を使用する必要があるのか​​」という質問に対する回答のトップに非常にがっかりしました。ではない、実際にどちらかそれらの答え、あなたが言及した言語のほとんどでその広範な使用にもかかわらず、再帰に対して極めてバイアス警告を気にしない(そこにあなたが言ったことについて、具体的何も悪いことではありませんが、あなたは、問題とunderexaggeratingを誇張しているように見えます有用性)。
Bernhard Barker

2
あなたはおそらく正しい@Dukelingです。文脈については、私がこの回答を書いたとき、すでに書かれた再帰についての多くの素晴らしい説明がありました、そして私はその情報への補助であることを意図してこれを書きました、トップの回答ではありません。実際には、ツリーをウォークしたり、他のネストされたデータ構造を処理したりする必要がある場合、通常は再帰を使用しますが、実際には自分で作成したスタックオーバーフローをまだヒットしていません。
Peter Burns

63

再帰の簡単な英語の例。

A child couldn't sleep, so her mother told her a story about a little frog,
    who couldn't sleep, so the frog's mother told her a story about a little bear,
         who couldn't sleep, so the bear's mother told her a story about a little weasel... 
            who fell asleep.
         ...and the little bear fell asleep;
    ...and the little frog fell asleep;
...and the child fell asleep.

1
up + for heart touching :)
Suhail Mumtaz Awan

中国の民話で眠りに落ちない小さな子供のためにこのような同様の話があります、私はそれを思い出しました、そしてそれは現実世界の再帰がどのように機能するかを思い出させます。
Harvey Lin、

49

コンピュータサイエンスの最も基本的な意味では、再帰はそれ自体を呼び出す関数です。リンクされたリスト構造があるとします。

struct Node {
    Node* next;
};

そして、あなたはリンクリストが再帰でこれを行うことができる長さを知りたいです:

int length(const Node* list) {
    if (!list->next) {
        return 1;
    } else {
        return 1 + length(list->next);
    }
}

(これはもちろんforループでも実行できますが、概念の説明として役立ちます)


@Christopher:これは再帰の素晴らしく単純な例です。具体的には、これは末尾再帰の例です。ただし、Andreasが述べたように、forループを使用して簡単に(より効率的に)書き換えることができます。私の回答で説明しているように、再帰にはより良い使い方があります。
スティーブウォーサム

2
ここで本当にelseステートメントが必要ですか?
Adrien Be

1
いいえ、わかりやすくするためだけにあります。
Andreas Brinck 2012年

@SteveWortham:これは、書かれているように末尾再帰ではありません。後者が結果に1を加えることができるように、length(list->next)まだ戻る必要がありますlength(list)。ここまでの長さを渡すように記述されていた場合にのみ、呼び出し元が存在することを忘れることができました。のようにint length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }
cHao

46

関数がそれ自体を呼び出してループを作成するときはいつでも、それが再帰です。何でもそうですが、再帰には良い使い方と悪い使い方があります。

最も単純な例は、関数の最後の行がそれ自体の呼び出しである末尾再帰です。

int FloorByTen(int num)
{
    if (num % 10 == 0)
        return num;
    else
        return FloorByTen(num-1);
}

ただし、これは不十分でほとんど意味のない例です。これは、より効率的な反復で簡単に置き換えることができるためです。結局のところ、再帰は関数呼び出しのオーバーヘッドの影響を受けます。これは、上記の例では、関数自体の内部の操作と比較してかなりの量になる可能性があります。

したがって、反復ではなく再帰を行う理由はすべて、呼び出しスタックを利用して賢いことを行うためです。たとえば、同じループ内で異なるパラメーターを使用して関数を複数回呼び出す場合、それは分岐を実行する方法です。典型的な例は、シェルピンスキーの三角形です。

ここに画像の説明を入力してください

再帰を使用すると、これらの1つを非常に簡単に描画できます。この場合、呼び出しスタックは3方向に分岐します。

private void BuildVertices(double x, double y, double len)
{
    if (len > 0.002)
    {
        mesh.Positions.Add(new Point3D(x, y + len, -len));
        mesh.Positions.Add(new Point3D(x - len, y - len, -len));
        mesh.Positions.Add(new Point3D(x + len, y - len, -len));
        len *= 0.5;
        BuildVertices(x, y + len, len);
        BuildVertices(x - len, y - len, len);
        BuildVertices(x + len, y - len, len);
    }
}

イテレーションで同じことを行おうとすると、達成するのにより多くのコードが必要になると思います。

その他の一般的な使用例には、階層のトラバース、たとえばWebサイトクローラー、ディレクトリ比較などがあります。

結論

実際には、反復分岐が必要な場合はいつでも、再帰が最も理にかなっています。


27

再帰は、分割統治の考え方に基づいて問題を解決する方法です。基本的な考え方は、元の問題を取り、それをそれ自体の小さな(より簡単に解決できる)インスタンスに分割し、それらの小さなインスタンスを(通常は同じアルゴリズムを再度使用して)解決してから、それらを最終的なソリューションに再構成することです。

正規の例は、nの階乗を生成するルーチンです。nの階乗は、1からnまでのすべての数値を乗算して計算されます。C#の反復ソリューションは次のようになります。

public int Fact(int n)
{
  int fact = 1;

  for( int i = 2; i <= n; i++)
  {
    fact = fact * i;
  }

  return fact;
}

反復的なソリューションには驚くべきことは何もありません。C#に精通している人なら誰でも理解できるはずです。

再帰的解は、n番目の階乗がn * Fact(n-1)であることを認識することで見つかります。別の言い方をすると、特定の階乗数がわかっている場合は、次の階乗数を計算できます。C#での再帰的なソリューションは次のとおりです。

public int FactRec(int n)
{
  if( n < 2 )
  {
    return 1;
  }

  return n * FactRec( n - 1 );
}

この関数の最初の部分はベースケース(またはガード句)と呼ばれ、アルゴリズムが永久に実行されるのを防ぎます。関数が1以下の値で呼び出されると、値1を返すだけです。2番目の部分はより興味深く、「再帰的ステップ」として知られています。ここで、わずかに変更されたパラメーターを使用して同じメソッドを呼び出し(1ずつデクリメント)、結果をnのコピーで乗算します。

最初に遭遇したときこれは一種の混乱を招く可能性があるので、実行時にそれがどのように機能するかを調べることは有益です。FactRec(5)を呼び出すと想像してください。ルーチンに入ると、ベースケースに拾われないため、次のようになります。

// In FactRec(5)
return 5 * FactRec( 5 - 1 );

// which is
return 5 * FactRec(4);

パラメーター4を指定してメソッドに再び入ると、再びガード節によって停止されないため、次のようになります。

// In FactRec(4)
return 4 * FactRec(3);

この戻り値を上記の戻り値に代入すると、

// In FactRec(5)
return 5 * (4 * FactRec(3));

これにより、最終的な解決策がどのように到達するかについての手掛かりが得られるので、途中の各ステップをすばやく追跡して表示します。

return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));

この最終的な置換は、ベースケースがトリガーされたときに行われます。この時点で、最初に階乗の定義に直接等しい、解決する単純な代数公式があります。

メソッドへのすべての呼び出しは、ベースケースがトリガーされるか、パラメータがベースケースに近い同じメソッドへの呼び出し(再帰呼び出しと呼ばれることが多い)になることに注意してください。そうでない場合、メソッドは永久に実行されます。


2
良い説明ですが、これは単に末尾再帰であり、反復解よりも優れているわけではないことに注意することが重要です。ほぼ同じ量のコードであり、関数呼び出しのオーバーヘッドのために実行が遅くなります。
スティーブウォー

1
@SteveWortham:これは末尾再帰ではありません。再帰的なステップでは、戻る前にの結果をFactRec()乗算する必要がありますn
rvighne

12

再帰は、それ自体を呼び出す関数の問題を解決しています。これの良い例は階乗関数です。階乗は、5の階乗がたとえば5 * 4 * 3 * 2 * 1の場合の数学の問題です。この関数は、C#で正の整数をテストします(テストされていない-バグがある可能性があります)。

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}

9

再帰とは、問題のより小さなバージョンを解決し、その結果に加えて他の計算を使用して元の問題の答えを定式化することによって問題を解決する方法を指します。多くの場合、小さいバージョンを解決するプロセスでは、メソッドは問題のさらに小さいバージョンを解決し、解決するのは簡単な「基本ケース」に達するまで続きます。

たとえば、数の階乗を計算するには、次のようXに表すことができX times the factorial of X-1ます。したがって、メソッドはの階乗を見つけるために「再帰」し、最終的な答えを与えるためX-1に得られたものXを乗算します。もちろん、の階乗を見つけるにはX-1、最初にの階乗を計算しますX-2。ときに基本ケースのようになりX、それが戻るために知っている場合には、0又は1である1ので0! = 1! = 1


1
私は何をしているrefereingは再帰ではないと思いますが、<a href=" en.wikipedia.org/wiki/...とConquer</a>アルゴリズムの設計原理。<A HREF =」で、例えばルックen.wikipedia。 org / wiki / Ackermann_function ">アッカーマン関数</a>
GabrielŠčerbák10年

2
いいえ、私はD&Cのことではありません。D&Cは、2つ以上の副問題が存在することを意味します。再帰自体はありません(たとえば、ここで示す階乗の例はD&Cではなく、完全に線形です)。D&Cは本質的に再帰のサブセットです。
アンバー

3
あなたがリンクした正確な記事からの引用:「分割統治アルゴリズムは、問題を同じ(または関連する)タイプの2つ以上のサブ問題に再帰的に分解することによって機能します」
Amber

厳密に言えば、再帰は問題を解決する必要がないので、それは素晴らしい説明ではないと思います。あなたは自分自身を呼び出すことができます(そしてオーバーフロー)。
UK-AL

私はPHPマスターのために書いている記事であなたの説明を使っていますが、それをあなたに帰することはできません。よろしくお願いします。
霜降りの素晴らしい

9

古いよく知られた問題を考えてみましょう:

数学では、2つ以上のゼロ以外の整数の最大公約数(gcd)…は、剰余なしで数値を除算する最大の正の整数です。

gcdの定義は驚くほど簡単です:

gcdの定義

ここでmodはモジュロ演算子(つまり、整数除算後の剰余)です。

英語では、この定義は、任意の数値とゼロの最大公約数がその数であることを示し、2つの数値mnの最大公約数はnの最大公約数であり、mnで除算した後の余りです。

これが機能する理由を知りたい場合は、ユークリッドアルゴリズムに関するウィキペディアの記事をご覧ください。

例としてgcd(10、8)を計算してみましょう。各ステップはその直前のステップと同じです。

  1. gcd(10、8)
  2. gcd(10、10 mod 8)
  3. gcd(8、2)
  4. gcd(8、8 mod 2)
  5. gcd(2、0)
  6. 2

最初のステップでは、8はゼロに等しくないため、定義の2番目の部分が適用されます。10 mod 8 = 2なので、8は一度に10になり、残りは2になります。ステップ3で2番目の部分が再度適用されますが、今回は8 mod 2 = 0なので、2は剰余なしで8を割ります。ステップ5では、2番目の引数は0なので、答えは2です。

等号の左側と右側の両方にgcdが表示されていることに気づきましたか?数学者は、式は、あなたが定義しているので、この定義は再帰的であると言うでしょう再発するが、その定義内。

再帰的な定義はエレガントになる傾向があります。たとえば、リストの合計の再帰的な定義は次のとおりです。

sum l =
    if empty(l)
        return 0
    else
        return head(l) + sum(tail(l))

どこheadリストの最初の要素があるとtail、リストの残りの部分です。sum最後にその定義内で繰り返されることに注意してください。

多分あなたは代わりにリストの最大値を好むでしょう:

max l =
    if empty(l)
        error
    elsif length(l) = 1
        return head(l)
    else
        tailmax = max(tail(l))
        if head(l) > tailmax
            return head(l)
        else
            return tailmax

非負整数の乗算を再帰的に定義して、一連の加算に変えることができます。

a * b =
    if b = 0
        return 0
    else
        return a + (a * (b - 1))

乗算を一連の加算に変換することについて意味がない場合は、いくつかの簡単な例を拡張して、それがどのように機能するかを確認してください。

マージソートには素敵な再帰的な定義があります:

sort(l) =
    if empty(l) or length(l) = 1
        return l
    else
        (left,right) = split l
        return merge(sort(left), sort(right))

何を探すべきかわかっていれば、再帰的な定義はあちこちにあります。これらの定義のすべては非常にシンプルなベースケースを持っているかに注意してください、例えば、GCD(M、0)=メートル。再帰的なケースは問題を回避し、簡単な答えを導き出します。

この理解があれば、ウィキペディアの再帰に関する記事の他のアルゴリズムに感謝することができます


8
  1. 自分自身を呼び出す関数
  2. 関数を(簡単に)簡単な操作に加えて、問題の一部に同じ関数を追加できる場合。むしろ、これは再帰の良い候補になると言うべきです。
  3. 彼らはします!

正規の例は次のような階乗です。

int fact(int a) 
{
  if(a==1)
    return 1;

  return a*fact(a-1);
}

一般に、再帰は必ずしも高速ではなく(再帰関数は小さい傾向があるため、関数呼び出しのオーバーヘッドが高くなる傾向があります。上記を参照)、いくつかの問題(スタックオーバーフローは誰か?)に悩まされる可能性があります。自明ではないケースでは「正しく」理解するのが難しい傾向にあると言う人もいますが、私は実際にはそうはいきません。状況によっては、再帰が最も理にかなっており、特定の関数を記述する最もエレガントで明確な方法です。一部の言語は再帰的ソリューションを支持し、それらをはるかに最適化していることに注意してください(LISPが思い浮かびます)。


6

再帰関数とは、それ自体を呼び出す関数です。私がそれを使用することがわかった最も一般的な理由は、ツリー構造をトラバースすることです。たとえば、チェックボックス付きのTreeViewがある場合(新しいプログラムのインストール、「インストールする機能の選択」ページを考えてください)、次のような「すべてチェック」ボタン(疑似コード)が必要になる場合があります。

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

function checkRecursively(Node n) {
    n.Checked = True;
    foreach ( n.Children as child ) {
        checkRecursively(child);
    }
}

つまり、checkRecursivelyは最初に渡されたノードをチェックし、次にそのノードの子ごとに自分自身を呼び出すことがわかります。

再帰については少し注意する必要があります。無限再帰ループに入ると、スタックオーバーフロー例外が発生します:)

必要に応じて使うべきではない理由は考えられません。状況によっては役立ちますが、状況によっては役立ちません。

面白いテクニックなので、一部のプログラマーは、正当な理由なしに、必要以上に頻繁にそれを使用することになると思います。これにより、一部のサークルでは再帰に悪い名前が付けられました。


5

再帰は、それ自体を直接または間接的に参照する式です。

単純な例として再帰的な頭字語を考えてみましょう:

  • GNUGNU's Not Unixの
  • PHPPHPの略です。ハイパーテキストプリプロセッサ
  • YAMLYAML Ai n't Markup Languageの略です
  • WINEWine Is an Emulatorの
  • VISAVisa International Service Associationの略です

ウィキペディアのその他の例


4

再帰は、私が「フラクタル問題」と呼んでいるものと最もうまく機能します。この場合、大きなものの小さなバージョンで構成された大きなものを処理します。それぞれのバージョンは、大きなもののさらに小さなバージョンです。ツリーやネストされた同一の構造などをトラバースまたは検索する必要がある場合は、再帰の候補になりそうな問題が発生しています。

人々は多くの理由で再帰を避けます:

  1. ほとんどの人(私も含む)は、関数型プログラミングとは対照的に、手続き型プログラミングまたはオブジェクト指向プログラミングでプログラミングの歯を磨きました。このような人々にとって、反復的なアプローチ(通常はループを使用)はより自然に感じられます。

  2. 手続き型プログラミングまたはオブジェクト指向プログラミングでプログラミングの歯を削った人たちは、エラーが発生しやすいため、再帰を回避するように言われることがよくあります。

  3. 再帰は遅いとよく言われます。ルーチンの呼び出しとルーチンからの戻りには、多くのスタックのプッシュとポップが含まれ、ループよりも低速です。一部の言語はこれを他の言語よりもうまく処理すると思います。これらの言語は、支配的なパラダイムが手続き型またはオブジェクト指向である言語ではない可能性が高いです。

  4. 私が使用した少なくともいくつかのプログラミング言語については、スタックがそれほど深くないために、特定の深さを超えた場合に再帰を使用しないようにという推奨を聞いたことを覚えています。


4

再帰的ステートメントとは、次の処理のプロセスを入力とすでに行った処理の組み合わせとして定義するステートメントです。

たとえば、階乗を取る:

factorial(6) = 6*5*4*3*2*1

しかし、factorial(6)も簡単に確認できます。

6 * factorial(5) = 6*(5*4*3*2*1).

だから一般的に:

factorial(n) = n*factorial(n-1)

もちろん、再帰に関してトリッキーなことは、すでに行ったことに関して物事を定義したい場合、開始する場所が必要になるということです。

この例では、factorial(1)= 1を定義して、特別なケースを作成します。

今、私たちはそれを下から上に見ていきます:

factorial(6) = 6*factorial(5)
                   = 6*5*factorial(4)
                   = 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1

factorial(1)= 1を定義したので、「ボトム」に到達します。

一般的に言えば、再帰的な手順には2つの部分があります。

1)再帰部分。同じ手順を介して「すでに実行」したことと組み合わせた新しい入力の観点からいくつかの手順を定義します。(つまりfactorial(n) = n*factorial(n-1)

2)ベースパーツ。開始する場所を指定することにより、プロセスが永久に繰り返されないようにします(つまりfactorial(1) = 1

最初に頭を動かすのは少し混乱するかもしれませんが、たくさんの例を見るだけですべてがまとまるはずです。概念をより深く理解したい場合は、数学的帰納法を研究してください。また、一部の言語は再帰呼び出しに最適化されていますが、他の言語は最適化されていないことに注意してください。注意しないと、めちゃくちゃ遅い再帰関数を作成するのは簡単ですが、ほとんどの場合、パフォーマンスを向上させる手法もあります。

お役に立てれば...


4

私はこの定義が好きです
。再帰では、ルーチンは問題自体の小さな部分を解決し、問題を小さな部分に分割し、それ自体を呼び出して小さな部分をそれぞれ解決します。

また、再帰に関するコンピュータサイエンスの本で使用されている例を批判する、Code Completeでの再帰に関するSteve McConnellsの議論も気に入っています。

階乗やフィボナッチ数に再帰を使用しないでください

コンピュータサイエンスの教科書に関する問題の1つは、再帰の愚かな例を示していることです。典型的な例は、階乗の計算またはフィボナッチ数列の計算です。再帰は強力なツールであり、どちらの場合にもそれを使用するのは本当に馬鹿げています。私のために働いたプログラマーが階乗を計算するために再帰を使用した場合、私は他の誰かを雇うでしょう。

これは非常に興味深い指摘であり、再帰がよく誤解されている理由かもしれません。

編集:これはダブの答えを掘り下げたものではありませんでした-私がこれを投稿したとき、私はその返信を見ていませんでした


6
階乗またはフィボナッチ数列が例として使用される理由のほとんどは、それらが再帰的な方法で定義される一般的な項目であるため、再帰の例にそれらを計算するために自然に役立ちます-それが実際には最良の方法ではない場合でもCSの観点から。
アンバー

私は同意します-私が本を読んでいたときに、再帰に関するセクションの途中で提起するのは興味深いポイントであることがわかりました
Robben_Ford_Fan_boy

4

1.)メソッドはそれ自体を呼び出すことができる場合、再帰的です。直接:

void f() {
   ... f() ... 
}

または間接的に:

void f() {
    ... g() ...
}

void g() {
   ... f() ...
}

2.)再帰を使用する場合

Q: Does using recursion usually make your code faster? 
A: No.
Q: Does using recursion usually use less memory? 
A: No.
Q: Then why use recursion? 
A: It sometimes makes your code much simpler!

3.)反復コードの記述が非常に複雑な場合にのみ、再帰を使用します。たとえば、プレオーダー、ポストオーダーなどのツリートラバーサル手法は、反復的および再帰的の両方にすることができます。しかし、通常は単純であるため、再帰を使用します。


パフォーマンスに関する分割と征服の際の複雑さの軽減についてはどうですか?
mfrachet

4

これは簡単な例です:セット内の要素数。(物事を数えるより良い方法がありますが、これは素晴らしい単純な再帰的な例です。)

まず、2つのルールが必要です。

  1. セットが空の場合、セット内のアイテムの数はゼロです(そうです!)。
  2. セットが空でない場合、カウントは1に1つのアイテムが削除された後のセット内のアイテムの数を足したものになります。

次のようなセットがあるとします:[xxx]。アイテムの数を数えてみましょう。

  1. セットは空ではない[xxx]なので、ルール2を適用します。アイテム数は、1に[xx]のアイテム数を加えたものです(つまり、アイテムを削除しました)。
  2. セットは[xx]なので、ルール2を再度適用します。1つ+ [x]の項目数。
  3. セットは[x]であり、これもルール2に一致します。1つ+ []内のアイテム数。
  4. これで、セットは[]になり、ルール1に一致します。カウントはゼロです!
  5. ステップ4(0)の答えがわかったので、ステップ3(1 + 0)を解くことができます。
  6. 同様に、ステップ3(1)の答えがわかったので、ステップ2(1 + 1)を解くことができます。
  7. そして最後に、ステップ2(2)の答えがわかったので、ステップ1(1 + 2)を解いて、[xxx]内のアイテム数、つまり3を取得できます。

これは次のように表すことができます。

count of [x x x] = 1 + count of [x x]
                 = 1 + (1 + count of [x])
                 = 1 + (1 + (1 + count of []))
                 = 1 + (1 + (1 + 0)))
                 = 1 + (1 + (1))
                 = 1 + (2)
                 = 3

再帰的ソリューションを適用する場合、通常、少なくとも2つのルールがあります。

  • 基本、すべてのデータを「使い果たした」ときに何が起こるかを示す単純なケース。これは通常、「処理するデータが不足している場合、答えはXです」のバリエーションです。
  • まだデータがある場合にどうなるかを示す再帰ルール。これは通常、「データセットを小さくするために何かを行い、小さい方のデータセットにルールを再適用する」というルールの一種です。

上記を疑似コードに変換すると、次のようになります。

numberOfItems(set)
    if set is empty
        return 0
    else
        remove 1 item from set
        return 1 + numberOfItems(set)

他にも多くの人がカバーすると確信しているもっと便利な例(たとえば、ツリーをトラバースする)があります。


3

まあ、それはあなたが持っているかなりまともな定義です。ウィキペディアにも良い定義があります。そこで、別の(おそらくもっと悪い)定義を追加します。

人々が「再帰」に言及するとき、彼らは通常、自分の書いた関数が、その作業が完了するまで繰り返し呼び出される関数について話している。再帰は、データ構造の階層をトラバースするときに役立ちます。


3

例:階段の再帰的な定義は、次のとおりです。階段は、次のものから構成されます。


2

解決した問題を再帰するために、何もしないで完了です。
未解決の問題について再帰するには、次の手順を実行してから、残りの問題を再帰します。


2

わかりやすい英語で:次の3つのことができると仮定します。

  1. りんごを一個
  2. タリーマークを書き留めます
  3. タリーマークを数える

テーブルの前にたくさんのリンゴがあり、リンゴの数を知りたいとします。

start
  Is the table empty?
  yes: Count the tally marks and cheer like it's your birthday!
  no:  Take 1 apple and put it aside
       Write down a tally mark
       goto start

完了するまで同じことを繰り返すプロセスは、再帰と呼ばれます。

これがあなたが探している「分かりやすい英語」の答えであることを願っています!


1
待ってください。テーブルの前にたくさんのタリーマークがあります。今、タリーマークの数を知りたいのですが。これにリンゴをどうにかして使用できますか?
ChristofferHammarström10年

リンゴを地面から取り出して(プロセス中にそれらを置いたとき)、リストのタリーマークを1つスクラッチするたびに、タリーマークがなくなるまでテーブルに置きます。きっとあなたが持っていたタリーマークの数と同じ数のリンゴがテーブルに残ります。今すぐそれらのリンゴを数えてすぐに成功します!(注:このプロセスはもはや再帰ではなく、無限ループです)
Bastiaan Linders

2

再帰関数は、それ自体への呼び出しを含む関数です。再帰的な構造体は、それ自体のインスタンスを含む構造体です。この2つを再帰クラスとして組み合わせることができます。再帰的なアイテムの重要な部分は、それ自体のインスタンス/呼び出しが含まれていることです。

2つのミラーが向かい合っていることを考慮してください。彼らが生み出すきちんとした無限の効果を見てきました。各反射はミラーのインスタンスであり、ミラーの別のインスタンス内に含まれています。それ自体の反射を含むミラーは再帰です。

バイナリ検索ツリーは、再帰の良いプログラミング例です。構造は再帰的であり、各ノードにはノードの2つのインスタンスが含まれます。二分探索木を操作する関数も再帰的です。


2

これは古い質問ですが、ロジスティクスの観点から(つまり、アルゴリズムの正確性の観点やパフォーマンスの観点からではなく)回答を追加したいと思います。

仕事にはJavaを使用していますが、Javaはネストされた関数をサポートしていません。そのため、再帰を実行する場合、外部関数を定義する必要があります(これは、コードがJavaの官僚的規則にぶつかるためだけに存在します)、またはコードを完全にリファクタリングする必要がある場合があります(これは私が本当に嫌いです)。

したがって、再帰自体は本質的にスタック操作であるため、私はしばしば再帰を避け、代わりにスタック操作を使用します。



1

プログラミングに適用される再帰とは、基本的に、タスクを実行するために、異なるパラメーターを使用して、独自の定義の内部(内部)から関数を呼び出すことです。


1

「私がハンマーを持っているなら、すべてが釘のように見えるようにしてください。」

再帰は、大きな問題の問題解決戦略であり、すべてのステップで、同じハンマーで毎回「2つの小さなものを1つの大きなものに変える」というものです。

あなたの机が1024の紙のまとまりのない混乱で覆われているとしましょう。再帰を使用して、混乱からきれいな紙のスタックをどのように作成しますか?

  1. 分割:すべてのシートを広げて、各「スタック」に1枚のシートができるようにします。
  2. 征服:
    1. 周りを回って、各シートを他のシートの上に置きます。これで2のスタックができました。
    2. 周りを回って、各2スタックを別の2スタックの上に置きます。スタックは4になりました。
    3. 周りを回って、各4スタックを別の4スタックの上に置きます。これで8個のスタックができました。
    4. ... 延々と ...
    5. これで、1024枚の大きなスタックが1つできました。

すべてを数えることを除いて、これはかなり直感的であることに注意してください(厳密に必要なわけではありません)。実際には、1シートのスタックまでは下がらないかもしれませんが、実際にはそうすることができます。重要な部分はハンマーです。腕を使って、常に1つのスタックを他のスタックの上に置いて、より大きなスタックを作成できます。どちらのスタックの大きさも(理由の範囲内で)問題ではありません。


6
あなたは分断統治を説明しています。これは再帰のですが、決してこれだけではありません。
Konrad Rudolph、

それはいいです。ここでは、[再帰の世界] [1]を文章に捉えようとしているのではありません。直感的な説明が欲しい。[1]:facebook.com/pages/Recursion-Fairy/269711978049
Andres Jaan Tack

1

再帰は、メソッド呼び出しiselfが特定のタスクを実行できるようにするプロセスです。コードの冗長性を減らします。ほとんどの再帰関数またはメソッドには、再帰呼び出しを中断するための条件が必要です。つまり、条件が満たされた場合にそれ自体が呼び出されないようにする必要があります。これにより、無限ループが作成されなくなります。すべての関数が再帰的に使用するのに適しているわけではありません。


1

ちょっと申し訳ありませんが、私の意見が誰かに同意する場合、私は単純な英語で再帰を説明しようとしています。

ジャック、ジョン、モーガンの3人のマネージャーがいるとします。ジャックは2人のプログラマー、ジョン-3、およびモーガン-5を管理します。あなたはすべてのマネージャーに300ドルを与え、それがどれくらいの費用がかかるかを知りたいと思っています。答えは明白ですが、モーガンの従業員のうち2人がマネージャーでもある場合はどうなりますか?

ここに再帰があります。階層の最上位から始めます。夏の費用は0ドルです。あなたはジャックから始め、次に彼に従業員としてマネージャーがいるかどうかを確認します。それらが見つかった場合は、マネージャとして従業員がいるかどうかなどを確認します。あなたがマネージャーを見つけるたびに、夏のコストに300ドルを追加します。ジャックの作業が終了したら、ジョンとその従業員に行き、次にモーガンに行きます。

答えを得るまでにどれだけのサイクルが必要かはわかりませんが、マネージャーの数と予算の数はわかっています。

再帰は、枝と葉を持つツリーであり、それぞれ親と子と呼ばれます。再帰アルゴリズムを使用すると、多かれ少なかれ意識的にデータからツリーを構築しています。


1

単純な英語では、再帰とは何度か何度か繰り返すことを意味します。

プログラミングの1つの例は、それ自体の中で関数を呼び出すことです。

数値の階乗を計算する次の例を見てください。

public int fact(int n)
{
    if (n==0) return 1;
    else return n*fact(n-1)
}

1
平易な英語では、何かを何度も繰り返すことを反復と呼びます。
toon81

1

基本的にデータ型の各ケースのケースを持つスイッチステートメントで構成される場合、アルゴリズムはデータ型の構造的再帰を示します。

たとえば、型で作業しているとき

  tree = null 
       | leaf(value:integer) 
       | node(left: tree, right:tree)

構造的再帰アルゴリズムは次の形式になります

 function computeSomething(x : tree) =
   if x is null: base case
   if x is leaf: do something with x.value
   if x is node: do something with x.left,
                 do something with x.right,
                 combine the results

これは、データ構造で機能するアルゴリズムを記述する最も明白な方法です。

ここで、ペアノ公理を使用して定義された整数(まあ、自然数)を見ると

 integer = 0 | succ(integer)

整数の構造的再帰アルゴリズムは次のようになります

 function computeSomething(x : integer) =
   if x is 0 : base case
   if x is succ(prev) : do something with prev

あまりに有名な階乗関数は、この形式の最も平凡な例です。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.