web-dev-qa-db-ja.com

再帰アルゴリズムを反復アルゴリズムに変換するための設計パターン

再帰アルゴリズムを反復アルゴリズムに変換するために使用できる一般的なヒューリスティック、ヒント、トリック、または一般的な設計パラダイムはありますか?私はそれができることを知っています、そうするときに心に留めておく価値のある慣習があるかどうか疑問に思っています。

43
fbrereto

多くの場合、再帰アルゴリズムの元の構造を完全に保持できますが、テールコールを使用してcontinuation-passingに変更することで、スタックを回避できます。 このブログエントリ 。 (私は本当により良いスタンドアロンの例を作るべきです。)

30
Brian

再帰アルゴリズムを反復アルゴリズムで置き換えるプロセスで使用する一般的な手法は、通常、スタックを使用して、再帰関数に渡されるパラメーターをプッシュすることです。

次の記事を確認してください。

22
CMS

一般的な方法は「実行する必要のあるもの」の実行リストを保持するLIFOスタックを管理するであり、whileループでプロセス全体を処理して、リストが空です。
このパターンでは、真の再帰モデルでの再帰呼び出しは次のように置き換えられます
-現在の(部分的に終了した)タスクの「コンテキスト」をスタックにプッシュします。
-新しいタスク(再帰を促したタスク)をスタックにプッシュ
-およびwhileループを「継続」する(つまり、最初にジャンプする)ため。ループの先頭近くで、ロジックは最後に挿入されたコンテキストをポップし、これに基づいて作業を開始します。

事実上これは単に「情報を移動する」これは、そうでなければ「システム」スタックのネストされたスタックフレームに保持されていたはずであり、アプリケーションが管理するスタックコンテナに。ただし、このスタックコンテナは任意の場所に割り当てることができるため、これは改善です(通常、再帰制限は「システム」スタックの制限に関連付けられています)。したがって、基本的に同じ作業が行われますが、「スタック」を明示的に管理することで、再帰的な呼び出しではなく、単一のループ構造内でこれを実行できます。

8
mjv

多くの場合、アキュムレータで部分的な結果を収集し、それを再帰呼び出しで渡すことにより、一般的な再帰を末尾再帰で置き換えることができます。末尾再帰は本質的に反復的であり、再帰呼び出しはジャンプとして実装できます。

たとえば、階乗の標準的な一般的な再帰的な定義

factorial(n) = if n = 0 then 1 else n * factorial(n - 1)

で置き換えることができます

 factorial(n) = f_iter(n, 1)

そして

 f_iter(n, a) = if n = 0 then a else f_iter(n - 1, n * a)

これは末尾再帰です。と同じです

a = 1;
while (n != 0) {
    a = n * a;
    n = n - 1;
}
return a;
7
starblue

私は通常、基本ケース(すべての再帰関数に1つある)から始めて、必要に応じて結果をキャッシュ(配列またはハッシュテーブル)に格納して、逆方向に作業します。

再帰関数は、小さなサブ問題を解決し、それらを使用して問題のより大きなインスタンスを解決することにより、問題を解決します。各副問題もさらに細かく分解され、副問題が非常に小さくなって解が簡単になる(つまり、基本ケース)まで続きます。

問題は、ベースケース(1つまたは複数)から始めて、それを使用してより大きなケースのソリューションを構築し、次にそれらを使用してさらに大きなケースなどを構築し、問題全体が解決するまでです。これはスタックを必要とせず、ループで実行できます。

簡単な例(Pythonの場合):

#recursive version
def fib(n):
     if n==0 or n==1:
             return n
     else:
             return fib(n-1)+fib(n-2)

#iterative version
def fib2(n):
     if n==0 or n==1:
             return n
     prev1,prev2=0,1 # start from the base case
     for i in xrange(n):
             cur=prev1+prev2 #build the solution for the next case using the previous solutions
             prev1,prev2=cur,prev1
     return cur
4
MAK

パフォーマンスの例については、これらのリンクをご覧ください

再帰VS反復(ループ):速度とメモリの比較

そして

再帰を反復で置き換える

そして

再帰vs反復

Q:通常、再帰バージョンはより高速ですか? A:いいえ-通常は遅くなります(スタックを維持するオーバーヘッドのため)

  Q: Does the recursive version usually use less memory?
  A: No -- it usually uses more memory (for the stack).

  Q: Then why use recursion??
  A: Sometimes it is much simpler to write the recursive version (but

本当に良い例を見るには、ツリーについて話し合うまで待つ必要があります...)

4
Adriaan Stander

1つのパターンは Tail Recursion です。

関数呼び出しは、その値を返す以外に関数が戻った後に何もしない場合、末尾再帰と呼ばれます。

Wiki

3
Mitch Wheat