web-dev-qa-db-ja.com

反復よりも再帰が優先されるのはなぜですか?

反復は再帰よりもパフォーマンスが高いですよね?それでは、なぜ反復よりも再帰のほうが優れている(言葉で言えばよりエレガントな)と意見する人がいますか? Haskellのような一部の言語が反復を許可せず、再帰を推奨しない理由が本当にわかりません。パフォーマンスが悪いものを奨励するのは不合理ではありませんか(そして、よりパフォーマンスの高いオプション、つまり再帰が利用可能な場合も)これに光を当ててください。ありがとう。

61
Debashish12

反復は再帰よりもパフォーマンスが高いですよね?

必ずしも。この概念は、再帰的であるかどうかに関係なく、関数の呼び出しに大きなオーバーヘッドがあり、呼び出しごとに新しいスタックフレームが作成される、多くのC言語に似ています。

多くの言語ではこれは当てはまらず、再帰は反復バージョンと同等以上のパフォーマンスを発揮します。最近では、一部のCコンパイラでさえ、一部の再帰構造を反復バージョンに書き換えたり、スタックフレームを末尾再帰呼び出しに再利用したりしています。

62
nos

深さ優先の検索を再帰的かつ反復的に実装してみて、どの検索が簡単だったかを教えてください。または、ソートをマージします。多くの問題については、関数スタックにデータを残すのではなく、独自のスタックを明示的に維持することになります。

Haskellを使ったことがないので話すことはできませんが、これはあなたのタイトルで提起された質問のより一般的な部分に対処するためです。

36
danben

反復には可変状態(インデックス)が含まれるため、Haskellは反復を許可しません。

14
kennytm

他の人が述べたように、再帰について本質的にパフォーマンスが低いものはありません。遅くなる言語もありますが、それは普遍的なルールではありません。

そうは言っても、私にとって再帰は、理にかなっているときに使用するツールです。再帰としてより適切に表現されるアルゴリズムがいくつかあります(ちょうどいくつかが反復を介してより適切であるように)。

適例:

fib 0 = 0
fib 1 = 1
fib n = fib(n-1) + fib(n-2)

それよりも意図を明確にする可能性のある反復的なソリューションは想像できません。

9
RHSeeger

Cの再帰と反復の長所と短所に関する情報を次に示します。

http://www.stanford.edu/~blp/writings/clc/recursion-vs-iteration.html

ほとんどの場合、再帰は反復よりも理解しやすい場合があります。

5
John Boker

反復は、再帰の特別な形式です。

4
Don Stewart

いくつかのこと:

  1. 反復は必ずしも高速ではありません
  2. すべての悪の根源:適度に速いかもしれないという理由だけで何かを奨励するのは時期尚早です。他の考慮事項があります。
  3. 多くの場合、再帰はより簡潔かつ明確に意図を伝えます
  4. 可変状態を一般的に回避することにより、関数型プログラミング言語は推論とデバッグが容易になり、再帰はこの例です。
  5. 再帰は反復よりも多くのメモリを必要とします。
4
user24359

再帰は、理論的にはエレガントまたは効率的と思われるものの1つですが、実際には一般的に(コンパイラまたは動的再コンパイラがコードを実行する場合を除いて)効率が低下します。一般に、特に複数の引数がプッシュ/ポップされる場合、不要なサブルーチン呼び出しを引き起こすものはすべて遅くなります。プロセッササイクル、つまりプロセッサが噛まなければならない命令を削除するためにできることはすべて公平なゲームです。コンパイラは一般的に最近ではかなり良い仕事をすることができますが、効率的なコードを手で書く方法を知ることは常に良いことです。

3
hjorgan

常に一方が他方より優れていると推論するのは難しいと思います。

ユーザーファイルシステムでバックグラウンド作業を行う必要があるモバイルアプリで作業しています。バックグラウンドスレッドの1つは、更新されたデータをユーザーに維持するために、ファイルシステム全体を時々スイープする必要があります。そのため、Stack Overflowを恐れて、反復アルゴリズムを作成しました。今日、私は同じ仕事のために再帰的なものを書きました。驚いたことに、反復アルゴリズムの方が高速です:再帰-> 37秒、反復-> 34秒(まったく同じファイル構造を処理)。

再帰:

private long recursive(File rootFile, long counter) {
            long duration = 0;
            sendScanUpdateSignal(rootFile.getAbsolutePath());
            if(rootFile.isDirectory()) {
                File[] files = getChildren(rootFile, MUSIC_FILE_FILTER);
                for(int i = 0; i < files.length; i++) {
                    duration += recursive(files[i], counter);
                }
                if(duration != 0) {
                    dhm.put(rootFile.getAbsolutePath(), duration);
                    updateDurationInUI(rootFile.getAbsolutePath(), duration);
                }   
            }
            else if(!rootFile.isDirectory() && checkExtension(rootFile.getAbsolutePath())) {
                duration = getDuration(rootFile);
                dhm.put(rootFile.getAbsolutePath(), getDuration(rootFile));
                updateDurationInUI(rootFile.getAbsolutePath(), duration);
            }  
            return counter + duration;
        }

反復:-再帰的バックトラッキングを使用した反復深さ優先検索

private void traversal(File file) {
            int pointer = 0;
            File[] files;
            boolean hadMusic = false;
            long parentTimeCounter = 0;
            while(file != null) {
                sendScanUpdateSignal(file.getAbsolutePath());
                try {
                    Thread.sleep(Constants.THREADS_SLEEP_CONSTANTS.TRAVERSAL);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                files = getChildren(file, MUSIC_FILE_FILTER);

                if(!file.isDirectory() && checkExtension(file.getAbsolutePath())) {
                    hadMusic = true;
                    long duration = getDuration(file);
                    parentTimeCounter = parentTimeCounter + duration;
                    dhm.put(file.getAbsolutePath(), duration);
                    updateDurationInUI(file.getAbsolutePath(), duration);
                }

                if(files != null && pointer < files.length) {
                    file = getChildren(file,MUSIC_FILE_FILTER)[pointer];
                }
                else if(files != null && pointer+1 < files.length) {
                    file = files[pointer+1];
                    pointer++;
                }
                else {
                    pointer=0;
                    file = getNextSybling(file, hadMusic, parentTimeCounter);
                    hadMusic = false;
                    parentTimeCounter = 0;
                }
            }
        }

private File getNextSybling(File file, boolean hadMusic, long timeCounter) {
            File result= null;
            //se o file é /mnt, para
            if(file.getAbsolutePath().compareTo(userSDBasePointer.getParentFile().getAbsolutePath()) == 0) {
                return result;
            }
            File parent = file.getParentFile();
            long parentDuration = 0;
            if(hadMusic) { 
                if(dhm.containsKey(parent.getAbsolutePath())) {
                    long savedValue = dhm.get(parent.getAbsolutePath());
                    parentDuration = savedValue + timeCounter;
                }
                else {
                    parentDuration = timeCounter; 
                }
                dhm.put(parent.getAbsolutePath(), parentDuration);
                updateDurationInUI(parent.getAbsolutePath(), parentDuration);
            }

            //procura irmao seguinte
            File[] syblings = getChildren(parent,MUSIC_FILE_FILTER);
            for(int i = 0; i < syblings.length; i++) {
                if(syblings[i].getAbsolutePath().compareTo(file.getAbsolutePath())==0) {
                    if(i+1 < syblings.length) {
                        result = syblings[i+1];
                    }
                    break;
                }
            }
            //backtracking - adiciona pai, se tiver filhos musica
            if(result == null) {
                result = getNextSybling(parent, hadMusic, parentDuration);
            }
            return result;
        }

反復はエレガントではありませんが、現在は非効率的な方法で実装されていますが、再帰的なものよりも高速です。そして、私はそれをフルスピードで実行したくないので、それをよりよく制御し、ガベージコレクタがより頻繁に仕事をするようにします。

とにかく、私は1つのメソッドが他のメソッドより優れていることを当然とは思わず、現在再帰的な他のアルゴリズムを確認します。ただし、少なくとも上記の2つのアルゴリズムから、反復アルゴリズムは最終製品のアルゴリズムになります。

2
jfv

少なくとも抽象的には、再帰に関して本質的にパフォーマンスが低いものはないと思います。再帰は反復の特別な形式です。言語が再帰を適切にサポートするように設計されている場合、反復と同様に実行できる可能性があります。

一般に、再帰により、次の反復で前方に移動する状態(パラメーター)が明示されます。これにより、言語プロセッサが実行を並列化するのが簡単になります。少なくともそれは、言語設計者が活用しようとしている方向です。

2
Michael Burr

Javaでは、再帰的ソリューションは一般的に非再帰的ソリューションよりも優れています。 Cでは、逆の傾向があります。これは、適応コンパイル言語と事前コンパイル言語の両方に一般的に当てはまると思います。

編集:「一般的に」とは、60/40の分割のようなものを意味します。言語がメソッド呼び出しを効率的に処理することに大きく依存しています。 JITコンパイルは、インライン化の処理方法を選択し、最適化でランタイムデータを使用できるため、再帰を好むと思います。ただし、問題のアルゴリズムとコンパイラに大きく依存しています。 Javaは、再帰の処理についてより賢くなり続けています。

Java (PDFリンク) を使用した定量的調査結果。これらは主にソートアルゴリズムであり、以前のJava Virtualマシン(正しく読めば1.5.x)再帰的な実装を使用することにより、2:1または4:1のパフォーマンスの向上が得られることがあり、再帰が大幅に遅くなることはめったにありません。個人的な経験では、違いはそれほど顕著ではありませんが、再帰を賢明に使用すると、50%の改善が一般的です。

2
BobMcGee

再帰と爆発物を比較します。すぐに大きな成果を上げることができます。しかし、注意せずに使用すると、結果は悲惨なものになる可能性があります。

フィボナッチ数を計算する再帰の複雑さを証明することに非常に感銘を受けました here 。その場合の再帰の複雑さはO((3/2)^ n)ですが、反復はちょうどO(n)です。 c#で再帰が記述されたn = 46の計算には30分かかります!ワオ...

IMHO再帰は、エンティティの性質が再帰(ツリー、構文解析など)に適している場合にのみ使用してください。 「神聖な」再帰コードのパフォーマンスとリソース消費を精査する必要があります。

1
ivan_d

パフォーマンスが実際に何であるかを理解するのに役立つと思います。 このリンク は、完全に合理的にコーディングされたアプリが実際に最適化の余地をどのように持っているかを示しています-つまり43の係数!これは、反復と再帰とは何の関係もありませんでした。

アプリが調整された場合、再帰に対して保存されたサイクルが実際に違いを生む可能性があるポイントに到達します。

1
Mike Dunlavey

低レベルのITERATIONは、CXレジストリを処理して、ループ、そしてもちろんデータレジストリをカウントします。 RECURSIONは、それを処理するだけでなく、スタックポインターへの参照を追加して、以前の呼び出しの参照を保持し、その後戻る方法も保持します。

私の大学の先生は、あなたが反復で行うことはすべて反復で行うことができ、その逆も可能であると言いましたが、反復により反復する方が簡単な場合があります(よりエレガント)が、パフォーマンスレベルでは反復を使用する方が良いです。

1
MRFerocius

再帰は、反復の典型的な実装です。それは抽象化の単なる下位レベルです(少なくともPythonでは):

class iterator(object):
    def __init__(self, max):
        self.count = 0
        self.max = max

    def __iter__(self):
        return self

    # I believe this changes to __next__ in Python 3000
    def next(self):
        if self.count == self.max:
            raise StopIteration
        else:
            self.count += 1
            return self.count - 1

# At this level, iteration is the name of the game, but
# in the implementation, recursion is clearly what's happening.
for i in iterator(50):
    print(i)
1
orokusaki

「反復は再帰よりもパフォーマンスが高い」は、実際には言語やコンパイラに固有です。思い浮かぶのは、コンパイラがループの展開を行うときです。この場合に再帰的なソリューションを実装すると、かなり遅くなります。

これは、科学者(仮説のテスト)であり、あなたのツールを知ることの代価です...

0
Austin Salonen

反復は再帰よりもパフォーマンスが高いですよね?

はい

ただし、再帰データ構造に完全にマッピングする問題がある場合、より良い解決策は常に再帰的です

iterationsで問題を解決するふりをすると、スタックを再発明して、コードのelegant再帰バージョンと比較して、より厄介でいコード。

つまり、Iterationは常に再帰。 (Von Neumannアーキテクチャの場合)。したがって、ループが十分である場合でも、常に再帰を使用すると、パフォーマンスが低下します。

ループよりも再帰の方が高速ですか?

0
Lucio M. Tato