web-dev-qa-db-ja.com

再帰とループ

私は再帰とループの使用の両方が自然な解決策のように見える問題に直面しています。このような場合に慣習または「推奨される方法」はありますか? (明らかに、以下のように単純ではありません)

再帰

Item Search(string desired, Scope scope) {
    foreach(Item item in scope.items)
        if(item.name == desired)
            return item;

    return scope.Parent ? Search(desired, scope.Parent) : null;
}

Loop

Item Search(string desired, Scope scope) {
    for(Scope cur = scope; cur != null; cur = cur.Parent)
        foreach(Item item in cur.items)
            if(item.name == desired)
                return item;

    return null;
}
46
zildjohn01

私は次の場合に再帰的なソリューションを好みます:

  • 再帰の実装は、通常、反復アプローチでは不可能な方法で問題の構造的側面を活用するため、反復ソリューションよりもはるかに単純です。

  • このように再帰を実装する言語について話していると仮定すると、再帰の深さがスタックオーバーフローを引き起こさないことを合理的に保証できます

条件1はここでは当てはまらないようです。反復ソリューションはほぼ同じレベルの複雑さなので、反復ルートに固執します。

55
John Feminella

パフォーマンスが重要な場合は、両方のベンチマークを行い、合理的に選択します。そうでない場合は、スタックオーバーフローの可能性を考慮して、複雑さに基づいて選択します。

古典的な本からのガイドラインがありますプログラミングスタイルの要素(KernighanとPlaugerによる)そのアルゴリズムはデータ構造に従うべきです。つまり、多くの場合、再帰構造は再帰アルゴリズムを使用してより明確に処理されます。

27
RBerteig

再帰は、より簡単に理解できる形式で自然に再帰的なアルゴリズムを表現するために使用されます。 「自然に再帰的な」アルゴリズムとは、小さなサブ問題への回答から回答が構築され、さらに小さなサブ問題への回答から構築されるアルゴリズムです。たとえば、階乗の計算などです。

関数型ではないプログラミング言語では、反復アプローチは再帰アプローチよりもほぼ常に高速で効率的です。そのため、再帰を使用する理由は、速度ではなく明快さです。再帰的な実装が反復的な実装よりもless clearになってしまう場合は、どうしても避けてください。

この特定のケースでは、反復実装がより明確であると判断します。

18
Tyler McHenry

関数型言語を使用している場合(そうではないようです)、再帰を使用してください。そうでない場合、ループはプロジェクトで作業している他の誰でもよりよく理解されるでしょう。もちろん、いくつかのタスク(ディレクトリを再帰的に検索するなど)は、他のタスクよりも再帰に適しています。

また、コードを末尾の再帰用に最適化できない場合、ループはより安全です。

9
Ed S.

ループを使用します。読みやすく、理解しやすく(コードの読み取りは常に書くよりもずっと難しくなります)、一般的にははるかに高速です。

7
Emil H

すべての末尾再帰アルゴリズムをループに展開できること、およびその逆も同様です。一般的に、再帰アルゴリズムの再帰的実装は、ループの実装よりもプログラマーにとって明確であり、デバッグも容易です。また、一般的に言えば、ループ内のブランチ/ジャンプはスタックフレームのプッシュとポップよりも実行が速いため、実際のループ実装のパフォーマンスはより速くなります。

個人的に言えば、末尾再帰アルゴリズムの場合、最もパフォーマンスが集中する状況を除いて、再帰実装に固執することを好みます。

4
Not Sure

私はループを好む

  • 再帰はエラーを起こしやすい
  • すべてのコードは1つの関数/メソッドのままです
  • メモリと速度の節約

ループを機能させるためにスタック(LIFOスキーマ)を使用します

Javaでは、スタックはDequeインターフェースでカバーされます

// Get all the writable folders under one folder
// Java-like pseudocode
void searchWritableDirs(Folder rootFolder){
    List<Folder> response = new List<Folder>(); // Results
    Deque<Folder> folderDeque = new Deque<Folder>(); // Stack with elements to inspect
    folderDeque.add(rootFolder);
    while( ! folderDeque.isEmpty()){
        Folder actual = folder.pop(); // Get last element
        if (actual.isWritable()) response.add(actual); // Add to response
        for(Folder actualSubfolder: actual.getSubFolder()) { 
            // Here we iterate subfolders, with this recursion is not needed
            folderDeque.Push(actualSubfolder);
        } 
    } 
    log("Folders " + response.size());
 }

より複雑でなく、よりコンパクト

// Get all the writable folders under one folder
// Java-like pseudocode
void searchWritableDirs(Folder rootFolder){
    List<Folder> response = new List<Folder>(); // Results
    rec_searchWritableDirs(actualSubFolder,response);
    log("Folders " + response.size());
}

private void rec_searchWritableDirs(Folder actual,List<Folder> response) {
   if (actual.isWritable()) response.add(actual); // Add to response
   for(Folder actualSubfolder: actual.getSubFolder()) {
       // Here we iterate subfolders, recursion is needed
       rec_searchWritableDirs(actualSubFolder,response);
   }                
}

後者のほうがコードは少ないですが、2つの機能があり、コードを理解するのが難しくなります。

4
user898384

再帰バージョンの方が理解しやすいと思いますが、コメントのみです。

Item Search(string desired, Scope scope) {
    // search local items
    foreach(Item item in scope.items)
        if(item.name == desired)
            return item;

    // also search parent
    return scope.Parent ? Search(desired, scope.Parent) : null;
}

このバージョンの説明ははるかに簡単です。ループのバージョンにいいコメントを書いてみてください。

3
ypnos

私は再帰がより自然だと思っていますが、コンパイラが末尾呼び出しの最適化を行わず、ツリー/リストがスタックサイズに対して深すぎる場合、ループを使用することを余儀なくされる可能性があります。

2
Svante

私は通常、ループの使用を好みます。最も良いOOP設計により、再帰を使用せずにループを使用できます(したがって、プログラムがこれらの厄介なパラメーターとアドレスをすべてスタックにプッシュするのを停止します)。

手続き型コードでより多くの用途があり、再帰的な方法で考えるのがより論理的に思えます(簡単に状態またはメタデータ(情報?)を保存できないため)使用に値する状況をさらに作成します)。

再帰は、関数のプロトタイピングやベースの作成に適していますが、最適化フェーズでコードが機能し、そのコードに戻った後、ループに置き換えてみてください。

繰り返しますが、これはすべての意見です。最適なものを選択してください。

1
user865927

さて、私はたくさんの答えを見て、答えさえ受け入れましたが、正しいものを見たことはなく、理由を考えていました...

長い話の短い:

ループによって同じユニットを生成できる場合、常に再帰を避けます!

再帰はどのように機能しますか?

•スタックメモリ内のフレームは、単一の関数呼び出しに割り当てられています

•フレームには、実際のメソッドへの参照が含まれています

•メソッドにオブジェクトがある場合、オブジェクトはヒープメモリに配置され、フレームにはヒープメモリ内のそのオブジェクトへの参照が含まれます。

•これらの手順は、単一のメソッド呼び出しごとに実行されています!

リスク:

•スタックに新しい再帰メソッドを配置するメモリがない場合のStackOverFlow。

•ヒープに再帰的な保存オブジェクトを格納するメモリがない場合のOutOfMemory。

ループはどのように機能しますか?

•前のすべてのステップ。ただし、ループ内で繰り返しコードを実行しても、すでに消費されていればそれ以上データは消費されません。

リスク:

•条件がまったく存在しない場合、ループ内に単一のリスクがあります...クラッシュやその他の問題は発生しません。単純にwhile(true)を実行してもループは終了しません。

テスト:

ソフトウェアで次を実行します。

private Integer someFunction(){

return someFunction();
}

すぐにStackOverFlow例外を取得し、おそらくOutOfMemoryも例外を取得します

二番目に:

while(true){

}

ソフトウェアはフリーズするだけで、クラッシュは発生しません。

最後になりましたが-forループ:

常にforループを使用してください。この方法またはその方法で、このループは、ループを超えない限界点を与えることを余儀なくされます。確かに、あなたは非常に怒って、forループは停止することはありませんが、メモリ管理とソフトウェアの生産性向上のため、再帰の代わりに常にループを使用することをお勧めします。これは現在大きな問題です。

参照:

スタックベースのメモリ割り当て

1

より読みやすい形式でループを書くこともできます。 Cのfor(init;while;increment)には、incrementコマンドが開始時に言及されているが、ループの終わりに実行されるため、読みやすさの欠点があります。

また、2つのサンプルは同等ではありませんSearch(null,null)として呼び出すと、再帰サンプルは失敗し、ループは失敗しません。これにより、ループバージョンが改善されます。

変更されたサンプルは次のとおりです(nullがfalseであると仮定)

再帰(固定および末尾呼び出しの最適化可能)

Item Search(string desired, Scope scope) {

    if (!scope) return null

    foreach(Item item in scope.items)
        if(item.name == desired)
            return item;

    //search parent (recursive)
    return Search(desired, scope.Parent);
}

Loop

Item Search(string desired, Scope scope) {
    // start
    Scope cur = scope;

    while(cur) {
        foreach(Item item in cur.items)
            if(item.name == desired)
                return item;

        //search parent
        cur = cur.Parent;

    } //loop

    return null;
}
0
Lucio M. Tato

コードをコンパイルすると、ほとんど違いはありません。いくつかのテストを行い、使用されているメモリの量と実行速度を確認します。

0
Jay

作業しているシステムのスタックが小さい場合(組み込みシステム)、再帰の深さは制限されるため、ループベースのアルゴリズムを選択することが望まれます。

0
switchmode