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


181

redditのスレッドが明らかに興味深い質問を育てました。

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

投稿の(counter?)の例はペアです:

(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))

3
これが反例であるかはわかりません。スタック手法が機能します。それはきれいではありません、そして私はそれを書くつもりはありませんが、それは可能です。アクダスはあなたのリンクでそれを認めているようです。
Matthew Flaschen、2009年

あなたの(num-ways xy)は(x + y)choosex =(x + y)!/(x!y!)であり、再帰は必要ありません。
ShreevatsaR


再帰は便利なだけだと思います。
e2-e4 2018年

回答:


181

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

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

このコインの裏側では、式の再帰的な定義を以前の結果をメモするための招待状として扱うほど高度なプログラミングシステムを確実に想像できます。これにより、コンピュータに正確にどの手順を指示するかをわずらわせることなく、速度のメリットを提供できます。再帰的な定義を使った式の計算を続けます。ダイクストラはほぼ間違いなくそのようなシステムを想像しました。彼は長い間、実装をプログラミング言語のセマンティクスから切り離そうとしていました。次に、彼の非決定的でマルチプロセッシングのプログラミング言語は、実践的なプロのプログラマーよりも優れています。

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

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

このような状況では、元の言語に対する政治的抵抗を克服しようとする場合があります。Greenspun(頬の舌)の10番目の法則のように、Lispをうまく再実装していないことに気付くかもしれません。または、完全に異なるソリューションへのアプローチを見つけるだけかもしれません。しかし、いずれにしても、確かに方法があります。


10
Church-Turingはまだ証明されていませんか?
Liran Orevi 2009

15
@eyelidlessness:AをBに実装できる場合、それはBが少なくともAと同じくらいの能力を持っていることを意味します。A-implementation-of-BでAのステートメントを実行できない場合、それは実装ではありません。AをBに実装でき、BをAに実装できる場合、power(A)> = power(B)、およびpower(B)> = power(A)です。唯一の解決策はpower(A)== power(B)です。
Tordek

6
re:最初の段落:あなたはチャーチ-チューリングの論文ではなく、計算モデルの同等性について話しています。同等性は教会および/またはチューリングによって証明されたAFAIRでしたが、それは論文ではありません。論文は、直感的に計算できるものはすべて厳密な数学的な意味で計算できるという実験的な事実です(チューリングマシン/再帰関数などによって)。物理学の法則を使用して、チューリングマシンでは実行できないような問題(たとえば、問題の停止)を計算する非古典的なコンピューターを構築できた場合、それは反証される可能性があります。同等性は数学的定理であり、反証されません。
sdcvvc 2012

7
この回答は一体どのようにして肯定的な票を得ましたか?最初に、チューリングの完全性とChurch-Turingの論文を混ぜ合わせ、次に「高度な」システムについて言及し、遅延無限無限再帰をドロップします(これはCまたは任意のチューリング完全言語で実行できるためです。チューリング完全の意味を誰かが知っていますか?)。次に、これがオプラについての質問だったような、希望に満ちた手持ちの結論であり、必要なのは前向きで高揚することだけですか?恐ろしい答え!
ex0du5

8
そして、セマンティクスについてのbs ??? 本当に?これは構文変換についての質問です。どういうわけか、drop Dijkstraに名前を付ける優れた方法になり、パイ計算について何か知っていることを暗示していますか?これを明確にしておこう。言語の意味論のセマンティクスを見ても、他のモデルを見ても、この質問に対する答えには関係がない。言語がアセンブリであろうと、生成ドメインモデリング言語であろうと、何の意味もありません。チューリング完全性と「スタック変数」を「変数のスタック」に変換することについてのみです。
ex0du5

43

すべての再帰関数に対して非再帰フォームを書くことは常に可能ですか?

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

残念ながら、オンラインでGOTOの適切な正式な定義を見つけることができないので、次のようにします。

A GOTOプログラムは、コマンドのシーケンスであるP上で実行されるレジスタマシンように、Pは次のいずれかです。

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

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

詳細については、この回答を参照してください。


正解です。ただし、実際には、再帰的なアルゴリズムを反復的なアルゴリズムに変換するのは非常に困難です。たとえば、私はこれまでのところ、ここに提示された単相タイパーをcommunity.topcoder.com/…を反復アルゴリズムに変えることができませんでした
Nils

31

再帰は、実際のインタプリタまたはコンパイラでスタックまたは同様の構成要素として実装されます。そのため、再帰関数を繰り返し対応する関数に変換できます。これは、(自動的に行われる場合は)常に行われる方法だからです。コンパイラーの作業をその場限りで、おそらく非常に醜く非効率的な方法で複製することになります。


13

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

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


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

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

  • 深度優先トラバーサルはスタックを使用して実行でき、幅優先トラバーサルはキューを使用して実行できます。

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

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

Turingマシンは、単一のループを実行することで、すべての処理を実行します。

  1. 命令をフェッチし、
  2. それを評価し、
  3. goto 1。

7

はい、明示的にスタックを使用します(ただし、再帰の方がはるかに読みやすく、IMHOです)。


17
いつも読んだほうが楽しいとは言いません。反復と再帰の両方がその場所にあります。
Matthew Flaschen、2009年

6

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


スタックのデータ構造がスタックに割り当てられている場合は目的に反するか、ヒープに割り当てられている場合は時間がかかりますか?それは些細なことのように思えますが、私には非効率的です。
conradkleinespel 2014

1
@conradk場合によっては、コールスタックを使い尽くすのに十分な大きさの問題に対してツリー再帰操作を実行する必要がある場合に行うのが現実的です。ヒープメモリは通常、はるかに豊富です。
jamesdlin 16

4

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

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


2

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


1

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

ファッションは他のテクノロジーに移行しているので、これは今や問題の少ないものです。



0

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

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

このアプローチは相互に再帰的な関数で機能しますが、末尾呼び出しでしか機能しないと思います。

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


0

私は「はい」と言います-関数呼び出しは、(大まかに言えば)gotoとスタック操作にすぎません。あなたがする必要があるのは、関数の呼び出し中に構築されたスタックを模倣し、gotoのようなものを実行することです(このキーワードを明示的に持たない言語でgotoを模倣することもできます)。


1
私はOPが証明または実質的な何かを探していると思います
Tim

0

ウィキペディアの以下のエントリーをご覧ください。それらを出発点として使用して、質問に対する完全な回答を見つけることができます。

どこから始めるべきかについてのヒントを与えるかもしれない段落に従います:

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

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



-1

tazzego、再帰は、あなたが好きかどうかに関わらず、関数が自分自身を呼び出すことを意味します。再帰なしでできるかどうかというと、これは意味があり、「いいえ、再帰の定義に同意できないので、本当ではない」とは言えません。

それを念頭に置いて、あなたが言う他のほとんどすべてはナンセンスです。ナンセンスではないとあなたが言う他の唯一のことは、あなたがコールスタックなしでプログラミングを想像することができないという考えです。これは、コールスタックの使用が一般的になるまで数十年にわたって行われてきたことです。FORTRANの古いバージョンには呼び出しスタックがなく、問題なく動作しました。

ところで、ループの手段として再帰(SMLなど)のみを実装するチューリング完全言語が存在します。ループの手段として反復のみを実装するチューリング完全言語も存在します(例:FORTRAN IV)。Church-Turingの論文は、再帰のみの言語で可能なことはすべて非再帰言語で実行できること、そしてその両方にチューリング完全性の特性があるという事実によってその逆が可能であることを証明しています。


-3

反復アルゴリズムは次のとおりです。

def howmany(x,y)
  a = {}
  for n in (0..x+y)
    for m in (0..n)
      a[[m,n-m]] = if m==0 or n-m==0 then 1 else a[[m-1,n-m]] + a[[m,n-m-1]] end
    end
  end
  return a[[x,y]]
end
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.