web-dev-qa-db-ja.com

再帰または反復?

両方で同じ目的を果たすことができるアルゴリズムで、再帰の代わりにループを使用したり、その逆を使用したりすると、パフォーマンスが低下しますか?例:指定された文字列が回文であるかどうかを確認します。私は多くのプログラマが、単純な反復アルゴリズムが法案に適合できることを誇示する手段として再帰を使用しているのを見てきました。コンパイラは、何を使用するかを決定する際に重要な役割を果たしますか?

210
Omnipotent

再帰関数が tail recursive (最後の行は再帰呼び出し)であるかどうかによって、再帰のコストが高くなる可能性があります。末尾の再帰はコンパイラーによって認識されるべきであり(コード内にある簡潔で明確な実装を維持しながら)、その対応する反復に最適化されます。

私は、数か月または数年でコードを維持する必要がある貧弱な吸盤(自分自身または他の誰か)にとって最も意味のある方法でアルゴリズムを作成します。パフォーマンスの問題が発生した場合は、コードのプロファイルを作成してから、反復実装に移行して最適化を検討してください。 memoization および dynamic programming を調べてください。

176
Paul Osborne

ループにより、プログラムのパフォーマンスが向上する場合があります。再帰により、プログラマーのパフォーマンスが向上する場合があります。あなたの状況でより重要なものを選択してください!

309
Leigh Caldwell

再帰と反復を比較することは、プラスドライバーとマイナスドライバーを比較するようなものです。ほとんどの場合、平らな頭のプラスねじを外すことができますが、そのねじ用に設計されたドライバーを使用した方が簡単でしょうか?

一部のアルゴリズムは、設計方法(フィボナッチ数列、構造のようなツリーのトラバースなど)のために再帰に適しています。再帰により、アルゴリズムがより簡潔になり、理解しやすくなります(したがって、共有可能かつ再利用可能)。

また、いくつかの再帰アルゴリズムは、反復的な兄弟よりも効率的な「遅延評価」を使用します。これは、ループが実行されるたびにではなく、必要なときにのみ高価な計算を行うことを意味します。

これで開始できます。私もあなたのためにいくつかの記事と例を掘り下げます。

リンク1:Haskel vs PHP(Recursion vs Iteration)

プログラマがPHPを使用して大きなデータセットを処理しなければならなかった例を次に示します。彼は、再帰を使用してHaskelで処理するのがどれほど簡単かを示していますが、PHPには同じメソッドを実行する簡単な方法がないため、結果を取得するために反復を使用する必要がありました。

http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html

リンク2:再帰をマスターする

再帰の悪い評判のほとんどは、命令型言語の高コストと非効率性に起因しています。この記事の著者は、再帰アルゴリズムを最適化してより高速かつ効率的にする方法について説明しています。また、従来のループを再帰関数に変換する方法と、末尾の再帰を使用する利点についても説明します。彼の最後の言葉は、私の重要なポイントのいくつかを本当にまとめたものです。

「再帰的プログラミングにより、プログラマーは、保守可能で論理的に一貫性のある方法でコードを整理するより良い方法を得ることができます。」

https://developer.ibm.com/articles/l-recurs/

リンク3:再帰はループよりも高速ですか? (回答)

ここにあなたに似ているstackoverflow質問の答えへのリンクがあります。著者は、再帰またはループのどちらかに関連するベンチマークの多くは、very言語固有であると指摘しています。命令型言語は通常、ループを使用すると高速になり、関数型言語では再帰により低速になります。逆も同様です。このリンクからとるべき主な点は、言語にとらわれない/状況盲目の意味で質問に答えることは非常に難しいということだと思います。

再帰はループよりも高速ですか?

76
Swift

通常、各再帰呼び出しはメモリアドレスをスタックにプッシュする必要があるため、再帰はメモリのコストが高くなります。これにより、後でプログラムがそのポイントに戻ることができます。

それでも、ツリーを操作するときのように、再帰がループよりもはるかに自然で読みやすい場合が多くあります。これらの場合、再帰にこだわることをお勧めします。

16
Doron Yaacoby

通常、パフォーマンスの低下は別の方向にあると予想されます。再帰呼び出しは、追加のスタックフレームの構築につながる可能性があります。これに対するペナルティはさまざまです。また、Pythonのような一部の言語(より正確には、一部の言語の実装では...)では、再帰的に指定するタスク(たとえば、ツリーデータ構造。これらの場合、あなたは本当にループに固執したいです。

適切な再帰関数を記述すると、末尾再帰などを最適化するコンパイラがあると仮定すると、パフォーマンスの低下をいくらか軽減できます(また、関数が実際に末尾再帰であることを確認するために二重チェックを行います。オン。)

「エッジ」の場合(高性能コンピューティング、非常に大きな再帰深度など)を除き、意図を最も明確に表現し、適切に設計され、保守可能なアプローチを採用することが望ましいです。ニーズを特定した後にのみ最適化します。

11
zweiterlinde

複数の小さな断片に分解できる問題の場合、反復は反復よりも優れています。

たとえば、再帰的なFibonnaciアルゴリズムを作成するには、fib(n)をfib(n-1)とfib(n-2)に分解し、両方の部分を計算します。反復では、単一の機能を何度も繰り返すことができます。

しかし、フィボナッチは実際には壊れた例であり、反復は実際にはより効率的だと思います。 fib(n)= fib(n-1)+ fib(n-2)およびfib(n-1)= fib(n-2)+ fib(n-3)であることに注意してください。 fib(n-1)は2回計算されます!

より良い例は、ツリーの再帰アルゴリズムです。親ノードの分析の問題は、複数各子ノードの分析の小さな問題に分解できます。フィボナッチの例とは異なり、小さな問題は互いに独立しています。

そうです-再帰は、複数の、より小さい、独立した、同様の問題に分解できる問題の反復よりも優れています。

8
Ben

再帰を使用するとパフォーマンスが低下します。どの言語でもメソッドを呼び出すには多くの準備が必要になるためです:呼び出しコードはリターンアドレス、呼び出しパラメーター、プロセッサーレジスタなどの他のコンテキスト情報をどこかに保存し、リターン時に呼び出されたメソッドは戻り値をポストし、それが呼び出し側によって取得され、以前に保存されたコンテキスト情報が復元されます。反復アプローチと再帰アプローチのパフォーマンスの違いは、これらの操作にかかる時間にあります。

実装の観点から、呼び出しコンテキストを処理するのにかかる時間がメソッドの実行にかかる時間に匹敵する場合、違いに本当に気づき始めます。再帰的なメソッドの実行に、呼び出し元のコンテキスト管理部分よりも時間がかかる場合、一般的にコードが読みやすく理解しやすく、パフォーマンスの低下に気付かないため、再帰的な方法を使用します。それ以外の場合は、効率上の理由から反復してください。

7
entzik

Javaの末尾再帰は現在最適化されていないと思います。詳細は this LtUと関連リンクに関する議論全体に散らばっています。これは次のバージョン7の機能かもしれませんが、特定のフレームが欠落するため、スタック検査と組み合わせると特定の困難が明らかになります。スタック検査は、Java 2以降、きめ細かいセキュリティモデルを実装するために使用されています。

http://lambda-the-ultimate.org/node/13

6
Mike Edwards

多くの場合、キャッシュにより再帰が高速化され、パフォーマンスが向上します。たとえば、従来のマージルーチンを使用したマージソートの反復バージョンを次に示します。キャッシングによりパフォーマンスが向上するため、再帰的な実装よりも実行速度が遅くなります。

反復実装

public static void sort(Comparable[] a)
{
    int N = a.length;
    aux = new Comparable[N];
    for (int sz = 1; sz < N; sz = sz+sz)
        for (int lo = 0; lo < N-sz; lo += sz+sz)
            merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}

再帰的な実装

private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
    if (hi <= lo) return;
    int mid = lo + (hi - lo) / 2;
    sort(a, aux, lo, mid);
    sort(a, aux, mid+1, hi);
    merge(a, aux, lo, mid, hi);
}

PS-これは、コースラで提示されたアルゴリズムのコースについて、ケビン・ウェイン教授(プリンストン大学)から言われたものです。

5
Nikunj Banka

反復法よりもはるかに洗練されたソリューションを提供する多くのケースがあります。一般的な例はバイナリツリーのトラバースであるため、必ずしも維持するのが難しくありません。一般に、反復バージョンは通常少し高速です(最適化中は再帰バージョンを置き換える可能性があります)が、再帰バージョンは理解と実装が簡単です。

5
Felix

再帰は非常に便利です。たとえば、階乗を見つけるためのコードを考えます

int factorial ( int input )
{
  int x, fact = 1;
  for ( x = input; x > 1; x--)
     fact *= x;
  return fact;
}

再帰関数を使用して考えてみましょう

int factorial ( int input )
{
  if (input == 0)
  {
     return 1;
  }
  return input * factorial(input - 1);
}

これら2つを観察すると、再帰が理解しやすいことがわかります。ただし、注意して使用しないと、エラーが発生しやすくなります。 if (input == 0)を見逃した場合、コードはしばらく実行され、通常はスタックオーバーフローで終了します。

5
Harikrishnan

言語に依存します。 Javaでは、ループを使用する必要があります。関数型言語は再帰を最適化します。

4
mc

再帰を使用すると、各「反復」で関数呼び出しのコストが発生しますが、ループでは、通常支払うのは増分/減分だけです。したがって、ループのコードが再帰的ソリューションのコードよりもそれほど複雑でない場合、通常、ループは再帰よりも優れています。

4
MovEaxEsp

再帰と反復は、実装するビジネスロジックに依存しますが、ほとんどの場合、同じ意味で使用できます。ほとんどの開発者は、理解しやすいため、再帰に行きます。

4
Warrior

再帰は、反復のあらゆる定義よりも単純です(したがって、より基本的です)。 コンビネータのペア のみでチューリング完全システムを定義できます(はい、再帰自体もそのようなシステムの派生概念です)。 Lambda 微積分は、再帰関数を特徴とする同様に強力な基本システムです。ただし、反復を適切に定義する場合は、最初からもっと多くのプリミティブが必要です。

コードに関しては-いいえ、ほとんどのデータ構造は再帰的であるため、実際には、再帰的なコードは純粋に反復的なコードよりも理解および保守がはるかに簡単です。もちろん、それを正しくするためには、少なくとも、高階関数とクロージャーをサポートする言語が必要です。すべての標準のコンビネーターとイテレーターをきちんと取得するためです。もちろん、C++では、 FC++ などのハードコアユーザーでない限り、複雑な再帰的ソリューションは少しいように見えます。

3
SK-logic

リストを繰り返し処理する場合は、必ず繰り返してください。

他のいくつかの回答では、(深さ優先)ツリートラバーサルに言及しています。非常に一般的なデータ構造に対して行うのは非常に一般的なことなので、これは本当に素晴らしい例です。再帰はこの問題に対して非常に直感的です。

ここで「検索」メソッドを確認してください: http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html

3
Joe Cheng

(非テール)再帰では、関数が呼び出されるたびに(もちろん言語に依存して)新しいスタックを割り当てるなどのパフォーマンスヒットがあると思います。

2
metadave

C++では、再帰関数がテンプレート化されている場合、型推論と関数のインスタンス化はすべてコンパイル時に行われるため、コンパイラはそれを最適化する可能性が高くなります。最新のコンパイラは、可能であれば関数をインライン化することもできます。そのため、-O3-O2g++などの最適化フラグを使用する場合、再帰は反復よりも高速になる可能性があります。反復コードでは、コンパイラは既に多かれ少なかれ最適な状態にあるため(十分に記述されている場合)、最適化する機会が少なくなります。

私の場合、Armadillo行列オブジェクトを使用して、再帰的および反復的な方法で二乗することにより、行列のべき乗を実装しようとしました。アルゴリズムはここにあります... https://en.wikipedia.org/wiki/Exponentiation_by_squaring 。私の関数はテンプレート化され、1,000,00012x12の累乗10を計算しました。私は次の結果を得ました:

iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec

iterative + No-optimisation flag  -> 2.83.. sec
recursive + No-optimisation flag  -> 4.15.. sec

これらの結果は、gcc-4.8でc ++ 11フラグ(-std=c++11)を使用し、Armadillo 6.1でIntel mklを使用して取得されました。 Intelコンパイラも同様の結果を示しています。

2
Titas Chanda

再帰?どこから始めれば、wikiは「自己類似の方法でアイテムを繰り返すプロセスです」と表示します。

私がCをやっていた頃、C++の再帰は「Tail recursion」のようなものでした。また、再帰を使用する多くのソートアルゴリズムもあります。クイックソートの例: http://alienryderflex.com/quicksort/

再帰は、特定の問題に役立つ他のアルゴリズムと同様です。すぐに、または頻繁に使用できない場合がありますが、問題が発生した場合は喜んで利用できます。

2
Nickz

「再帰の深さ」に依存します。関数呼び出しのオーバーヘッドが総実行時間にどの程度影響するかによって異なります。

たとえば、古典的な階乗を再帰的に計算することは、次の理由により非常に非効率的です。-データオーバーフローのリスク-スタックオーバーフローのリスク-関数呼び出しのオーバーヘッドが実行時間の80%を占める

チェスのゲームでの位置分析用の最小-最大アルゴリズムを開発する一方で、後続のNの動きを分析する「分析の深さ」を再帰的に実装できます(^ _ ^をやっています)

2
ugasoft

マイクは正しい。末尾再帰はnot JavaコンパイラーまたはJVMによって最適化されます。次のようなもので常にスタックオーバーフローが発生します。

int count(int i) {
  return i >= 100000000 ? i : count(i+1);
}
1
noah

再帰には、再帰を使用して記述するアルゴリズムがO(n)スペースの複雑さを持つという欠点があります。反復アプローチにはO(1)のスペースの複雑さがあります。これは、再帰よりも反復を使用する利点です。次に、なぜ再帰を使用するのですか?

下記参照。

反復を使用して同じアルゴリズムを作成するのが少し難しいのに対して、再帰を使用してアルゴリズムを作成する方が簡単な場合があります。

1

許容されるスタックサイズに応じて、深すぎる再帰を使用するとスタックオーバーフローが発生することに注意する必要があります。これを防ぐには、再帰を終了するベースケースを指定してください。

1
Grigori A.

私の知る限り、Perlは末尾再帰呼び出しを最適化しませんが、偽造することはできます。

sub f{
  my($l,$r) = @_;

  if( $l >= $r ){
    return $l;
  } else {

    # return f( $l+1, $r );

    @_ = ( $l+1, $r );
    goto &f;

  }
}

最初に呼び出されたときに、スタック上のスペースを割り当てます。その後、引数を変更し、スタックに何も追加せずにサブルーチンを再起動します。したがって、自己を呼び出したことがないふりをし、反復プロセスに変更します。

my @_;」または「local @_;」は存在しないことに注意してください。これを実行した場合、動作しなくなります。

0
Brad Gilbert

反復がアトミックで、新しいスタックフレームをプッシュするよりも桁違いに高価な場合and新しいスレッドの作成and複数のコアがあるand yourランタイム環境ではこれらすべてを使用できますが、マルチスレッドと組み合わせた場合、再帰的アプローチによりパフォーマンスが大幅に向上する可能性があります。平均反復回数が予測できない場合は、スレッドの割り当てを制御し、プロセスが大量のスレッドを作成してシステムを占有するのを防ぐスレッドプールを使用することをお勧めします。

たとえば、一部の言語では、再帰的なマルチスレッドマージソートの実装があります。

ただし、繰り返しますが、マルチスレッドは再帰ではなくループで使用できるため、この組み合わせがうまく機能するかどうかは、OSやそのスレッド割り当てメカニズムなどの要因に依存します。

0
ccpizza

Chrome 45​​.0.2454.85 mだけを使用すると、再帰はニースの量よりも速くなるようです。

コードは次のとおりです。

(function recursionVsForLoop(global) {
    "use strict";

    // Perf test
    function perfTest() {}

    perfTest.prototype.do = function(ns, fn) {
        console.time(ns);
        fn();
        console.timeEnd(ns);
    };

    // Recursion method
    (function recur() {
        var count = 0;
        global.recurFn = function recurFn(fn, cycles) {
            fn();
            count = count + 1;
            if (count !== cycles) recurFn(fn, cycles);
        };
    })();

    // Looped method
    function loopFn(fn, cycles) {
        for (var i = 0; i < cycles; i++) {
            fn();
        }
    }

    // Tests
    var curTest = new perfTest(),
        testsToRun = 100;

    curTest.do('recursion', function() {
        recurFn(function() {
            console.log('a recur run.');
        }, testsToRun);
    });

    curTest.do('loop', function() {
        loopFn(function() {
            console.log('a loop run.');
        }, testsToRun);
    });

})(window);

結果

//標準forループを使用して100回実行

ループ実行用に100倍。完了までの時間:7.683ms

//末尾再帰を使用した関数再帰アプローチを使用した100回の実行

100x再帰実行。完了までの時間:4.841ms

下のスクリーンショットでは、テストごとに300サイクルで実行すると、再帰がより大きなマージンで再び勝ちます

Recursion wins again!

0
Alpha G33k