web-dev-qa-db-ja.com

再帰メソッドは常にJavaの反復メソッドより優れていますか?

再帰メソッドは常にJavaの反復メソッドより優れていますか?

また、それらは常に反復の代わりに使用できますか?

22
Hoon

再帰メソッドは常にJavaの反復メソッドより優れていますか?

いいえ

また、それらは常に反復の代わりに使用できますか?

always再帰関数を反復関数にすることができます。 (メモリで処理できる場合は、興味深いリンクを参照してください ここ

場合によっては、再帰を使用する方がよい場合があります(ツリーを処理するとき、バイナリツリーを移動するときなど)。私にとって、ループの使用が再帰より複雑ではるかに難しい場合は、ループを使用することを好みます。

再帰はより多くのメモリを使用しますが、より明確で読みやすい場合があります。ループを使用するとパフォーマンスが向上しますが、プログラマにとっては再帰の方が優れている場合があります(そして彼のパフォーマンス)。

したがって、結論として、何を使用するかを決定する-再帰または反復は、実装するもの、およびより重要なもの(可読性、パフォーマンス...)と、再帰または反復eleganceまたはperformanceを要求するようなものです。


factorialの次の2つの実装を検討してください。

反復的:

private int Factorial(int num)
{
    int result = 1;
    if (num <= 1) 
       return result;
    while (num > 1)
    {
        result * = num;
        num--;
    }
    return result;
}

再帰:

private int Factorial(int num)
{
    if (num <= 1) 
        return 1;
    return num * Factorial(num - 1);
}

読みやすい方法はどれですか?

明らかに再帰的なものであり、単純であり、最初の試行から記述して正常に実行できます。これは、数学の定義をJavaに変換するだけです。

どちらの方法がより効率的ですか?

例としてnum = 40、ここに時間の比較があります

long start = System.nanoTime();
int res = Factorial(40);
long end = System.nanoTime();
long elapsed = end - start;

System.out.println("Time: " + elapsed); 

2993は再帰的です

2138反復

もちろん、numが大きいほど、差は大きくなります。

27
Maroun

その他の回答の修正:反復は再帰に変換できます(スタックオーバーフローが発生する可能性があることに注意してください)。 ただし、すべての再帰を反復に直接変換できるわけではありません。多くの場合、スタックに格納されるデータを保持するために、何らかの形式のスクラッチ領域が必要になります。

どちらを選択するかは、使用する言語と再帰的ソリューションを作成する利便性によって異なります。


編集して、「直接」の意味を明確にします。

反復に直接変換する方法に基づいて、再帰には3つのタイプがあります。

  • メソッドの最後の操作が再帰呼び出しである、最適化可能な末尾呼び出し。これらは直接ループに変換できます。
  • 再帰呼び出しは最適化できません。呼び出し間で情報を保持する必要がありますが、スタックは必要ないためです。 fact(N)として表される階乗は古典的な例です。再帰的な定式化では、最後の演算は再帰的な呼び出しではなく、乗算です。これらのタイプの再帰呼び出しは、簡単に末尾呼び出し最適化形式に変換でき、そこから反復形式に変換できます(階乗を末尾呼び出し最適化形式に変換するには、2つの引数のバージョンを使用する必要がありますfact(n, acc) 、ここでaccは累積結果です)。
  • 各呼び出しで状態を保持する必要がある再帰。古典的な例は、プレオーダーツリートラバーサルです。呼び出しごとに、親ノードを覚えておく必要があります。 これを直接反復に変換する方法はありません。代わりに、各呼び出しからの状態を保持するために独自のスタックを構築する必要があります。
5
parsifal

再帰は、プログラマーがプログラムを理解するのに適していますが、多くの場合、スタックオーバーフローを引き起こし、反復よりも反復を優先します

事実は、再帰が問題を解決するための最も効率的なアプローチであることはめったになく、反復はほとんど常により効率的であることです。これは、再帰中に呼び出しスタックが非常に頻繁に使用されるため、通常、再帰呼び出しの実行に関連するオーバーヘッドが増えるためです。
これは、多くのコンピュータプログラミング言語が実際に必要な計算を実行するよりも、コールスタックの維持により多くの時間を費やすことを意味します。

再帰は反復よりも多くのメモリを使用しますか?一般的に言えば、そうです。これは、コールスタックが広範囲に使用されているためです。

再帰または反復を使用する必要がありますか?

再帰は、実装が簡単であり、通常、反復ソリューションよりも「エレガント」であるため、一般的に再帰が使用されます。再帰で行われることはすべて繰り返し実行することもできますが、再帰では一般にパフォーマンス上の欠点があることを覚えておいてください。ただし、解決しようとしている問題によっては、そのパフォーマンスの低下はごくわずかな場合があります。その場合、再帰を使用することは理にかなっています。再帰を使用すると、他のプログラマーがコードをより簡単に理解できるという追加の利点も得られます。これは常に良いことです。

2
amod

通常、再帰的なメソッドは数行のコードを必要としますが、アルゴリズムについて深く考えています。論理的な間違いをすると、おそらくStackOverflowErrorを取得します。

以下は、階乗の2つのプログラミング例です。

非アクティブ:

public int calculateIteractiveFactorial(int n) {
    // Base case
    if (n == 1) {
        return 1;
    }
    int factorial = 1;
    for (int i = 1; i <= n; i++) {
        factorial = factorial * i;
    }
    return factorial;
}

再帰:

public int calculateRecursiveFactorial(int n) {
    // Base case
    if (n == 1) {
        return 1;
    }
    return n * calculateRecursiveFactorial(n - 1);
}

コードの行、複雑さ、明快さ、まとまりなどを常に考慮しながら、各提案のさまざまなアイデアについて考えることは常に良いことです

0
axcdnt

「再帰は常に反復よりも優れている」という記述は誤りです。どちらか一方が他方よりも望ましい場合があります。

特定のケースに依存するため、どのタイプのアルゴリズムを使用するかについて決定的な答えを出すのは困難です。たとえば、フィボナッチ数列の一般的な教科書再帰の場合は、再帰を使用すると信じられないほど効率が悪いため、その場合は反復を使用することをお勧めします。逆に、ツリーのトラバースは、再帰を使用してより効率的に実装できます。

2番目の質問に答えるには、はい、反復アルゴリズムは再帰を使用して実装でき、その逆も可能です。

0
curena

どういう意味ですかすべての再帰は反復で実装することもでき、その逆も可能です。時々、再帰は理解しやすいです。ただし、再帰はスタックを膨らませるので、多くのネストされた呼び出しがメモリ不足の例外を引き起こす可能性があります。

0
omer schleifer

厳密に言えば、再帰と反復はどちらも同等に強力です。再帰的ソリューションは、スタックを使用した反復ソリューションとして実装できます。逆変換はトリッキーになる可能性がありますが、ほとんどの場合、呼び出しチェーンを通じて状態を渡すだけです。

Javaでは、再帰的なソリューションが(単純な)反復的なソリューションよりも優れている状況と、それらが基本的に同等である状況があります。他のほとんどの場合、関数呼び出しのオーバーヘッドが回避されるため、反復ソリューションの方が優れています。

関数が暗黙的にスタックを使用する場合、再帰的な解決策は通常優れています。深さ優先検索を考えてみましょう:

void walkTree(TreeNode t) {
    doSomething(t);

    if (t.hasLeft()) { walkTree(t.getLeft()); }
    if (t.hasRight()) { walkTree(t.getRight()); }
}

同等の反復解と比較

void walkTree(TreeNode t) {
    Stack<TreeNode> s = new Stack<TreeNode>();
    s.Push(t);
    while (!s.empty()) {
        TreeNode t = s.pop();
        doSomething(t);
        if (t.hasLeft()) { s.Push(t.getLeft()); }
        if (t.hasRight()) { s.Push(t.getRight()); }
    }
}

繰り返しの場合は、Stack<>オブジェクトによって作成されたガベージの代金を支払う必要があります。再帰的なケースでは、ガベージを作成したり、メモリを割り当てたりしないプロセススタックを使用します。再帰的ソリューションはスタックオーバーフローに対して脆弱ですが、それ以外の場合はより高速に実行されます。

関数が末尾再帰を使用する場合、JITは再帰的なものを反復的なものに変換するため、2つのソリューションは同等になります。テール再帰とは、関数が最後にそれ自体を再帰的に呼び出して、コンパイラーが蓄積された状態を無視できるようにすることです。たとえば、リストをたどる

void walkList(ListNode n) {
    doSomething(n);
    if (n.hasNext()) { walkList(n.getNext()); }
}

「walkList」は関数で最後に実行されるものであるため、JITはそれを本質的に「n = n.getNext(); goto begin」に変換し、反復ソリューションと同等にします。

他のほとんどの状況では、反復ソリューションが優れています。たとえば、幅優先検索を実行したい場合は、反復ソリューションでQueueの代わりにStackを使用して、それを瞬時に機能させながら、再帰的なソリューションでは、コールスタックの暗黙的なスタックの両方を支払う必要があり、キューの料金も支払う必要があります。

0
user295691

Javaの値渡しメカニズムを思い出すと、オブジェクト参照アドレスまたはプリミティブ型の値のコピーを渡すたびに、再帰により多くのメモリが消費されることがわかります。再帰呼び出しごとに渡すマッハパラメータは、各呼び出しで消費するマッハメモリとして使用します。一方、ループは簡単に使用できます。ただし、ツリーのMergesortや反復などの「分割統治」アルゴリズムがあることはわかっています。結果を返すために再帰を使用すると、より少ないステップを消費するので、これらのケースでは、再帰を使用する方が良いと思います。

0
Grigor Nazaryan