web-dev-qa-db-ja.com

すべての再帰を反復に変換できますか?

reddit thread は明らかに興味深い質問を提起しました:

末尾再帰関数は、簡単に反復関数に変換できます。他のものは、明示的なスタックを使用して変換できます。 every再帰を反復に変換できますか?

投稿の(カウンター?)例はペアです:

(define (num-ways x y)
  (case ((= x 0) 1)
        ((= y 0) 1)
        (num-ways2 x y) ))

(define (num-ways2 x y)
  (+ (num-ways (- x 1) y)
     (num-ways x (- y 1))
174
Tordek

再帰関数を常に反復関数に変えることはできますか?はい、絶対に、そしてメモリが役立つならば、教会チューリング論文はそれを証明します。簡単に言えば、再帰関数によって計算可能なものは反復モデル(チューリングマシンなど)によって計算可能であり、その逆も同様であると述べています。論文では、変換の方法を正確に説明していませんが、それは間違いなく可能であると言っています。

多くの場合、再帰関数の変換は簡単です。 Knuthは、「The Art of Computer Programming」でいくつかのテクニックを提供しています。多くの場合、再帰的に計算されたものは、より少ない時間とスペースで完全に異なるアプローチによって計算できます。この典型的な例は、フィボナッチ数またはそのシーケンスです。学位計画でこの問題に確実に遭遇しました。

このコインの裏側では、数式の再帰的な定義を以前の結果を記憶するための招待として扱うほど高度なプログラミングシステムを確実に想像できます。再帰的な定義を使用して式を計算します。ダイクストラはほぼ確実にそのようなシステムを想像していました。彼は実装をプログラミング言語のセマンティクスから分離しようとして長い時間を費やしました。再び、彼の非決定論的でマルチプロセッシングのプログラミング言語は、実践的なプロのプログラマーよりも優れています。

最終的な分析では、多くの関数は単純に理解しやすく、再帰形式で読み取り、書き込みが簡単です。説得力のある理由がない限り、これらの関数を(手動で)明示的に反復するアルゴリズムに変換しないでください。コンピューターはそのジョブを正しく処理します。

1つの説得力のある理由がわかります。 [donning asbestos underwear] Scheme、LISP、Haskell、OCaml、Perl、またはPascalのような超高級言語のプロトタイプシステムがあるとします。 CまたはJavaでの実装が必要になるような条件があるとします。 (おそらくそれは政治です。)それから、確かにいくつかの関数を再帰的に記述できますが、文字通り翻訳すると、ランタイムシステムが爆発します。たとえば、Schemeでは無限の末尾再帰が可能ですが、同じイディオムが既存のC環境に問題を引き起こします。別の例は、字句的にネストされた関数と静的スコープの使用です。Pascalはこれをサポートしていますが、Cはサポートしていません。

このような状況では、元の言語に対する政治的抵抗を克服しようとするかもしれません。 Greenspunの第10法則のように、LISPの再実装がひどい場合があります。または、ソリューションに対するまったく異なるアプローチを見つけるかもしれません。しかし、いずれにしても、確かに方法があります。

175
Ian

すべての再帰関数に対して非再帰形式を記述することは常に可能ですか?

はい。単純な形式的証明は、 µ recursion とGOTOなどの非再帰的計算の両方がチューリング完全であることを示すことです。すべてのチューリング完全計算の表現力は厳密に同等であるため、すべての再帰関数は非再帰チューリング完全計算によって実装できます。

残念ながら、GOTOのオンラインでの優れた正式な定義を見つけることができないため、以下にその1つを示します。

GOTOプログラムは、一連のコマンドPレジスタマシン で実行されるため、Pは次のいずれかです。

  • HALT、実行を停止します
  • r = r + 1ここで、rは任意のレジスタです
  • r = r – 1ここで、rは任意のレジスタです
  • GOTO xここで、xはラベルです
  • IF r ≠ 0 GOTO xここで、rは任意のレジスタであり、xはラベルです
  • 上記のコマンドのいずれかが続くラベル。

ただし、再帰関数と非再帰関数との間の変換は必ずしも簡単ではありません(コールスタックの無意識の手動再実装による場合を除く)。

詳細については、 this answer を参照してください。

39
Konrad Rudolph

再帰は、実際のインタープリターまたはコンパイラーでスタックまたは同様の構成体として実装されます。したがって、再帰関数を反復対応物に確実に変換することができます(自動の場合)常に実行されるので。コンパイラの作業をアドホックに、おそらく非常にく非効率的な方法で複製するだけです。

27
Vinko Vrsalovic

基本的には、本質的にあなたがしなければならないのは、メソッド呼び出し(暗黙的にスタックに状態をプッシュする)を明示的なスタックプッシュに置き換えて、「前の呼び出し」がどこに到達したかを覚えてから、「呼び出されたメソッド」を実行することです代わりに。

基本的にメソッド呼び出しをシミュレートすることで、ループ、スタック、ステートマシンの組み合わせをすべてのシナリオに使用できると思います。これが「より良い」(より速く、ある意味ではより効率的)ことになるかどうかは、一般的に言って実際には不可能です。

12
jerryjvl
  • 再帰的な関数実行フローはツリーとして表すことができます。

  • データ構造を使用してそのツリーをトラバースするループによって、同じロジックを実行できます。

  • 深さ優先の走査はスタックを使用して実行でき、幅優先の走査はキューを使用して実行できます。

答えは「はい」です。理由: https://stackoverflow.com/a/531721/2128327 .

単一のループで再帰を実行できますか?はい、なぜなら

チューリングマシンは、単一のループを実行することにより、すべてを実行します。

  1. 命令をフェッチし、
  2. それを評価し、
  3. 後藤1。
9
Khaled.K

はい、明示的にスタックを使用します(ただし、再帰は読みやすくなります)。

6
dfa

はい、非再帰バージョンを作成することは常に可能です。簡単な解決策は、スタックデータ構造を使用して、再帰的な実行をシミュレートすることです。

6
Heinzi

原則として、データ構造と呼び出しスタックの両方に対して無限の状態を持つ言語では、再帰を削除して繰り返しで置き換えることが常に可能です。これは、教会チューリング論文の基本的な結果です。

実際のプログラミング言語を考えると、答えはそれほど明白ではありません。問題は、プログラムに割り当てることができるメモリの量は限られているが、使用できる呼び出しスタックの量に制限がない言語(スタック変数のアドレスが32ビットのCアクセスできません)。この場合、再帰はより多くのメモリを使用できるため、より強力です。呼び出しスタックをエミュレートするのに十分な明示的に割り当て可能なメモリがありません。これに関する詳細な議論については、 この議論 を参照してください。

3
Zayenz

再帰の置き換えは、それよりもはるかに簡単な場合があります。再帰は、1990年代にCSで教えられていたファッショナブルなものであったため、当時の多くの平均的な開発者は、再帰で何かを解決した場合、より良い解決策でした。そのため、逆方向にループして逆方向にループしたり、そのような愚かなことをする代わりに、再帰を使用します。したがって、再帰を削除することは、単純な「当たり前」のタイプの運動である場合があります。

ファッションは他の技術にシフトしているので、これは今では問題ではありません。

1
Matthias Wandel

すべての計算可能な関数はチューリングマシンで計算できるため、再帰システムとチューリングマシン(反復システム)は同等です。

1
JOBBINE

ウィキペディアの次のエントリをご覧ください。質問に対する完全な回答を見つけるための出発点として使用できます。

どこから始めるかについてのヒントを提供する段落が続きます。

再帰関係を解くことは、 閉形式の解 :nの非再帰関数を取得することを意味します。

このエントリ の最後の段落もご覧ください。

0

再帰アルゴリズムを非再帰アルゴリズムに変換することは可能ですが、多くの場合、ロジックははるかに複雑であり、そのためにはスタックを使用する必要があります。実際、再帰自体はスタック、つまり関数スタックを使用します。

詳細: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions

0
Teoman shipahi

再帰の削除は複雑な問題であり、明確に定義された状況下で実行可能です。

以下のケースは簡単です:

0

私はイエスと言います-関数呼び出しはgotoとスタック操作(大まかに言えば)に他なりません。あなたがする必要があるのは、関数を呼び出している間に構築されたスタックを模倣し、gotoと同様のことをすることです(このキーワードも明示的に持たない言語でgotoを模倣することができます)。

0
sfussenegger

明示的なスタックのAppart、再帰を反復に変換する別のパターンは、トランポリンの使用です。

ここで、関数は最終結果を返すか、そうでなければ実行される関数呼び出しのクロージャーを返します。その後、開始(トランポリン)関数は、最終結果に到達するまで返されたクロージャーを呼び出し続けます。

このアプローチは相互に再帰的な関数で機能しますが、テールコールでのみ機能するのではないかと思います。

http://en.wikipedia.org/wiki/Trampoline_(computers)

0
Chris Vest