再帰を使用する場合


26

ループではなく再帰を使用する(比較的)基本的な(大学1年生のCSレベルの学生と考えてください)インスタンスはいつですか?


2
再帰をループに(スタックを使用して)変えることができます。
カベ

回答:


18

私は約2年間大学生にC ++を教え、再帰を扱ってきました。私の経験から、あなたの質問と感情は非常に一般的です。極端な場合、一部の学生は再帰を理解するのが難しいと見なしますが、他の学生はほとんどすべてに再帰を使用したいと考えています。

Daveはそれをうまくまとめていると思います。適切な場所で使用してください。つまり、自然に感じるときに使用します。うまく収まる問題に直面すると、おそらくそれを認識するでしょう。反復的な解決策を思い付くことすらできないように思えます。また、明快さはプログラミングの重要な側面です。他の人(そしてあなたも!)は、作成したコードを読んで理解できるはずです。反復ループは、再帰よりも一見理解しやすいと言っても安全だと思います。

プログラミングやコンピューターサイエンス全般をどれだけよく知っているかはわかりませんが、ここで仮想関数、継承、または高度な概念について話すことは意味がないと強く感じています。私はしばしばフィボナッチ数の計算の古典的な例から始めました。フィボナッチ数は再帰的に定義されるため、ここにうまく収まります。これは理解しやすいですし、必要としない任意の言語の派手な機能を。生徒が再帰の基本的な理解を得た後、以前に構築したいくつかの簡単な関数をもう一度見てみました。以下に例を示します。

文字列に文字が含まれていますか?x

これは以前の方法です。文字列を反復処理し、インデックスにが含まれているかどうかを確認します。x

bool find(const std::string& s, char x)
{
   for(int i = 0; i < s.size(); ++i)
   {
      if(s[i] == x)
         return true;
   }

   return false;
}

問題は、それを再帰的に実行できるかどうかです。確かに、ここに1つの方法があります。

bool find(const std::string& s, int idx, char x)
{
   if(idx == s.size())
      return false;

   return s[idx] == x || find(s, ++idx);
}

次の自然な質問は、このようにする必要がありますか?おそらくない。どうして?理解するのが難しく、思い付くのが難しいです。したがって、エラーも発生しやすくなります。


2
最後の段落は間違っていません。ただ頻繁に言及したいのですが、同じ推論は反復的な解決策よりも再帰的な解決策を支持します(クイックソート!)。
ラファエル

1
@Raphael Agreeed、正確に。反復的に表現するのが自然なものもあれば、再帰的に表現するものもあります。それが私がやろうとしていたポイントだった:)

うーん、私が間違っていても許してくれますが、例のコードで戻り行をif条件に分けて、xが見つかった場合にtrueを返し、そうでない場合は再帰部分にした方が良いと思いませんか?「or」が真であると判断されても実行を継続するかどうかはわかりませんが、その場合、このコードは非常に非効率的です。
-MindlessRanger

@MindlessRangerおそらく、再帰バージョンは理解や記述が難しいという完璧な例でしょうか?:-)
ジュホ

ええ、私の以前のコメントは間違っていました、「または」または「||」最初の条件が真の場合、次の条件をチェックし、そう何ineffiencyはありません
MindlessRanger

24

一部の問題の解決策は、再帰を使用してより自然に表現されます。

たとえば、2種類のノードを持つツリーデータ構造があると仮定します。整数値を格納するリーフ。ブランチには、フィールドに左右のサブツリーがあります。最低値が左端の葉にあるように、葉が順序付けられていると仮定します。

タスクがツリーの値を順番に印刷することであるとします。これを行うための再帰アルゴリズムは非常に自然です。

class Node { abstract void traverse(); }
class Leaf extends Node { 
  int val; 
  void traverse() { print(val); }
} 
class Branch extends Node {
  Node left, right;
  void traverse() { left.traverse(); right.traverse(); }
}

再帰なしで同等のコードを書くことははるかに困難です。それを試してみてください!

より一般的には、再帰はツリーのような再帰的なデータ構造のアルゴリズム、または自然にサブ問題に分割できる問題に対してうまく機能します。たとえば、アルゴリズムを分割して征服してください。

最も自然な環境で再帰を本当に見たい場合は、Haskellのような関数型プログラミング言語を検討する必要があります。このような言語では、ループ構造が存在しないため、すべてが再帰(または高次関数を使用して表現されますが、それは別の話であり、知っておく価値のあるものです)。

関数型プログラミング言語は、最適化された末尾再帰を実行することにも注意してください。これは、必要ない限りスタックフレームを配置しないことを意味します。本質的に、再帰はループに変換できます。実用的な観点からは、自然な方法でコードを記述できますが、反復コードのパフォーマンスを取得できます。記録的には、C ++コンパイラーも末尾呼び出しを最適化するようですので、C ++で再帰を使用することによる追加のオーバーヘッドはありません。


1
C ++には末尾再帰がありますか?関数型言語が通常行うことを指摘する価値があるかもしれません。
ルイス

3
ルイスありがとう。一部のC ++コンパイラは、末尾呼び出しを最適化します。(末尾再帰はプログラムのプロパティであり、言語ではありません。)答えを更新しました。
デイブクラーク

少なくともGCCは、テールコール(および一部の形式の非テールコール)を最適化します。
フォンブランド

11

再帰的に実際に生きている誰かから、私はこの主題にいくらかの光を当てようとします。

最初に再帰を導入したとき、それはそれ自体を呼び出す関数であり、基本的にツリートラバーサルなどのアルゴリズムで示されることがわかります。後で、LISPやF#などの言語の関数型プログラミングで多く使用されていることがわかります。私が書いたF#では、書いたもののほとんどは再帰的でパターンマッチングです。

F#などの関数型プログラミングの詳細を学ぶと、F#リストは単一リンクリストとして実装されていることがわかります。つまり、リストの先頭にのみアクセスする操作はO(1)であり、要素アクセスはO(n)です。これを学習すると、データをリストとしてトラバースし、新しいリストを逆順で作成し、リストを逆にしてから関数から戻る傾向があります。これは非常に効果的です。

これについて考え始めると、関数呼び出しが行われるたびに再帰関数がスタックフレームをプッシュし、スタックオーバーフローを引き起こす可能性があることにすぐに気付くでしょう。ただし、末尾呼び出しを実行できるように再帰関数を構築し、コンパイラーが末尾呼び出しのコードを最適化する機能をサポートしている場合。つまり、.NET OpCodes.Tailcallフィールドでは、スタックオーバーフローは発生しません。この時点で、ループを再帰関数として記述し、決定を一致として書き始めます。日ifwhileなりました歴史があります。

PROLOGなどの言語でバックトラッキングを使用してAIに移行すると、すべてが再帰的になります。これには命令型コードとはまったく異なる方法で考える必要がありますが、PROLOGが問題に適したツールである場合、大量のコードを記述する必要がなくなり、エラーの数を劇的に減らすことができます。参照:AmziカスタマーeoTek

再帰をいつ使用するかという質問に戻ります。プログラミングの見方の1つは、一方の端にハードウェアを使用し、もう一方の端に抽象的な概念を使用することです。問題のハードウェアに近いが、より多くの私はと命令型言語で考えるifwhile、より抽象的な問題は、より多くの私は、再帰で高レベルの言語で考えます。ただし、低レベルのシステムコードなどの記述を開始し、その有効性を検証したい場合は、再帰に大きく依存する定理証明のようなソリューションが役立ちます。

あなたが見ればジェーン・ストリート、あなたは彼らが関数型言語使用が表示されますOCamlのを。私は彼らのコードを見たことはありませんが、彼らが彼らのコードについて言及していることを読むことから、彼らは無意識に再帰的に考えています。

編集

使用法のリストを探しているので、コードで何を探すべきかという基本的な考え方と、基本を超えたカタモルフィズムの概念にほとんど基づいている基本的な使用法のリストを提供します。

C ++の場合:同じ構造またはクラスへのポインターを持つ構造またはクラスを定義する場合、ポインターを使用するトラバーサルメソッドの再帰を考慮する必要があります。

単純なケースは、一方向のリンクリストです。リストを先頭または末尾から処理し、ポインターを使用して再帰的にリストを走査します。

ツリーは、再帰がよく使用される別のケースです。あまりにも多くの場合、再帰なしでツリートラバーサルが表示される場合は、理由を尋ね始める必要がありますか?それは間違っていませんが、コメントで注意すべきことです。

再帰の一般的な用途は次のとおりです。


2
それは本当に素晴らしい答えのように聞こえますが、それはまた、すぐに私のクラスで教えられているものの少し上にあると私は信じています。
テイラーヒューストン

1
@TaylorHustonあなたは顧客であることを忘れないでください。あなたが理解したい概念を先生に尋ねてください。彼はおそらくクラスでそれらに答えないでしょうが、営業時間中に彼を捕まえて、将来多くの配当を支払うかもしれません。
ガイコーダ

いい答えですが、関数型プログラミングについて知らない人にとっては高度すぎるようです:)。
パッド

2
...素朴な質問者を導いて関数型プログラミングを研究する。勝つ!
-JeffE

8

他の回答で与えられたものよりも難解なユースケースを提供するために:再帰は、一般的なソースから派生したツリーのような(オブジェクト指向)クラス構造と非常によく混ざります。C ++の例:

class Expression {
public:
    // The "= 0" means 'I don't implement this, I let my subclasses do that'
    virtual int ComputeValue() = 0;
}

class Plus : public Expression {
private:
    Expression* left
    Expression* right;
public:
    virtual int ComputeValue() { return left->ComputeValue() + right->ComputeValue(); }
}

class Times : public Expression {
private:
    Expression* left
    Expression* right;
public:
    virtual int ComputeValue() { return left->ComputeValue() * right->ComputeValue(); }
}

class Negate : public Expression {
private:
    Expression* expr;
public:
    virtual int ComputeValue() { return -(expr->ComputeValue()); }
}

class Constant : public Expression {
private:
    int value;
public:
    virtual int ComputeValue() { return value; }
}

上記の例では再帰を使用しています:ComputeValueは再帰的に実装されています。サンプルを機能させるには、仮想関数と継承を使用します。Plusクラスの左右の部分は正確にはわかりませんが、気にする必要はありません。それは、独自の値を計算できるものです。

上記のアプローチの決定的な利点は、すべてのクラスが独自の計算を処理することです。考えられるすべての部分式の異なる実装を完全に分離します。それらは互いの動作に関する知識を持ちません。これにより、プログラムについての推論が容易になり、プログラムの理解、保守、拡張が容易になります。


1
あなたがどの「秘術」の例に言及しているのか分かりません。それにもかかわらず、OOとの統合に関する素晴らしい議論。
デイブクラーク

3

最初のプログラミングクラスで再帰を教えるために使用された最初の例は、数字のすべての数字を逆順に別々にリストする関数でした。

void listDigits(int x){
     if (x <= 0)
        return;
     print x % 10;
     listDigits(x/10);
}

またはそのようなもの(私はここでメモリから行っており、テストしていません)。また、より高いレベルのクラスに入ると、特に検索アルゴリズム、ソートアルゴリズムなどで再帰LOTを使用します。

そのため、現在の言語では役に立たない関数のように見えるかもしれませんが、長期的には非常に便利です。

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