ループではできない再帰でできることはありますか?


126

再帰を使用するほうがループを使用するよりも優れている場合と、ループを使用する方が再帰を使用するよりも優れている場合があります。「正しい」ものを選択すると、リソースを節約したり、コードの行数を減らすことができます。

ループではなく再帰を使用してのみタスクを実行できる場合はありますか?


13
私はそれを真剣に疑います。再帰は見栄えの良いループです。
軌道上の明るさのレース

6
答えが進む方向の違いを見て(そして、より良いものを提供することに失敗したばかりです)、もう少し背景とあなたがどのような答えを提供するかを支持するなら、誰かが好意に答えようとするかもしれません。仮想マシン(無制限のストレージとランタイム)の理論的証明が必要ですか?または実用的な例?(「ばかげて複雑になる」とは、「できない」と見なされる場合があります。)または何か違うのでしょうか?
5gon12eder

8
@LightnessRacesinOrbit私の英語を母国語としない人の耳には、「再帰は栄光のループです」という意味です。 。おそらく、私は「栄光の何か」イディオムを間違っていると解釈するでしょう。
ハイド

13
アッカーマン関数はどうですか?en.wikipedia.org/wiki/Ackermann_function、特に有用ではありませんが、ループ経由で行うことは不可能です。(このビデオを確認することもできますyoutube.com/watch?v=i7sm9dzFtEI by Computerphile)
WizardOfMenlo

8
@WizardOfMenlo befungeコードが実装されERREの(また、スタックと対話ソリューション...である)溶液。スタックを使用した反復アプローチは、再帰呼び出しをエミュレートできます。適切に強力なプログラミングでは、1つのループ構造を使用して別のループ構造をエミュレートできます。指示レジスタマシンはINC (r)JZDEC (r, z)チューリングマシンを実装することができます。「再帰」はありません。ゼロ以外の場合はジャンプです。アッカーマン関数が計算可能な場合(そうである場合)、その登録マシンはそれを行うことができます。

回答:


164

はいといいえ。最終的に、ループではできない再帰を計算することはできませんが、ループにはさらに多くの配管が必要です。したがって、再帰ではループではできないことの1つは、一部のタスクを非常に簡単にすることです。

木を歩いてください。再帰を使って木を歩くのは簡単です。それは世界で最も自然なことです。ループを使ってツリーを歩くのはそれほど簡単ではありません。スタックやその他のデータ構造を維持して、行ったことを追跡する必要があります。

多くの場合、問題の再帰的な解決策はきれいです。これは技術用語であり、重要です。


120
基本的に、再帰の代わりにループを実行することは、スタックを手動で処理することを意味します。
シルビウブルセア

15
... スタック(複数可)。次の状況では、複数のスタックを持つことを強くお勧めします。Aツリー内で何かを見つける再帰関数を考えてみましょう。そのものにA遭遇するたびBに、サブツリー内で関連するものを見つけた別の再帰関数を起動しAます。B再帰が完了するとA、に戻り、後者は独自の再帰を継続します。一つは、のために1つのスタックを宣言することAとのための1つB、または入れてB内部のスタックをAループ。単一のスタックを使用すると主張すると、事態は非常に複雑になります。
rwong

35
Therefore, the one thing recursion can do that loops can't is make some tasks super easy. また、ループでは再帰ではできないことの1つは、一部のタスクを非常に簡単にすることです。スタックを爆破しないように、ほとんどの自然反復的な問題を単純な再帰から末尾の再帰に変換するために必要ない、直感に反することを見ましたか?
メイソンウィーラー

10
以下のようなものを「物事が」より良い再帰オペレータ内にカプセル化することができ、時間の99%は@MasonWheeler mapまたはfold(あなたがそれらをプリミティブを検討することを選択した場合、実際に、私はあなたが使用することができると思うfold/ unfoldループの第3の代替として、または再帰)。ライブラリコードを記述している場合を除き、達成するはずのタスクではなく、反復の実装を心配する必要はほとんどありません。実際には、明示的なループと明示的な再帰はどちらも同様に貧弱です。トップレベルで避けるべき抽象化。
ルーシェンコ

7
部分文字列を再帰的に比較することで2つの文字列を比較できますが、不一致が発生するまで各文字を1つずつ比較するだけで、パフォーマンスが向上し、読みやすくなります。
スティーブンバーナップ

78

番号。

ダウンになって非常に計算するために必要な最低の基礎、あなただけ(これだけでは十分ではなく、必要な成分である)ループにできるようにする必要があります。それは問題ではありません

Turing Machineを実装できるプログラミング言語は、Turing completeと呼ばれます。そして、チューリングが完了している多くの言語があります。

「実際に動作する」という方法で私の好きな言語。チューリング完全でのことですFRACTRANあり、チューリング完全な。ループ構造は1つで、チューリングマシンを実装できます。したがって、計算可能なものはすべて、再帰を持たない言語で実装できます。したがって、単純なループでは不可能な計算可能性の観点から、再帰によって得られるもの何もありません。

これは本当にいくつかのポイントに要約されます:

  • 計算可能なものはすべてチューリングマシンで計算可能
  • チューリング機械を実装できる言語(チューリング完全と呼ばれる)は、他の言語ができることなら何でも計算できます
  • 再帰のない言語のチューリングマシンがあります(他のエソランのいくつかを取得したときにのみ再帰を行うものもあります)。ループ(そして、再帰ではできないループでは何もできません)。

これは、ループよりも再帰で、または再帰ではなくループでより簡単に考えられる問題クラスがあると言うことではありません。ただし、これらのツールも同様に強力です。

そして、私はこれを「エソラン」の極端なものにしたが(主にチューリングが完成し、かなり奇妙な方法で実装されているものを見つけることができるため)、これはエソランがオプションであることを意味するものではありません。Magic the Gathering、Sendmail、MediaWikiテンプレート、Scala型システムなど、偶然チューリングが完了しているものの完全なリストがあります。これらの多くは、実際に実行することに関しては最適とはほど遠いものであり、これらのツールを使用して計算可能なものを計算できるというだけです。


この等価性は、末尾呼び出しとして知られる特定の種類の再帰を実行するときに特に興味深いものになります。

次のように書かれた階乗法があるとしましょう。

int fact(int n) {
    return fact(n, 1);
}

int fact(int n, int accum) {
    if(n == 0) { return 1; }
    if(n == 1) { return accum; }
    return fact(n-1, n * accum);
}

このタイプの再帰はループとして書き直されます-スタックは使用されません。実際、このようなアプローチは、作成される同等のループよりもエレガントで理解しやすいことがよくありますが、繰り返しますが、再帰呼び出しごとに同等のループが作成され、ループごとに再帰呼び出しが作成される場合があります。

複雑とすることができ末尾呼び出し再帰呼び出しに単純なループを変換する時間もあり、より理解することは困難で。


理論的な側面については、Church Turingの論文をご覧ください。また、CS.SEのチャーチチューリングテーゼが役立つこともあります。


29
チューリングの完全性は、それが重要であるようにあまりにも多く投げかけられています。マジックギャザリングのように、多くのものがチューリングコンプリートですが、それはチューリングコンプリートの他の何かと同じという意味ではありません。少なくとも重要なレベルではありません。Magic the Gatheringで木を歩きたくありません。
スキャントロジャー

7
問題を「これはチューリングマシンと同等のパワーを持ちます」に減らすことができたら、そこに到達するだけで十分です。チューリングマシンはかなり低いハードルですが、必要なのはそれだけです。再帰ではできないループでできることはありません。逆の場合も同様です。

4
この答えでなされた発言はもちろん正しいですが、私はあえて議論が本当に説得力があるとは言いません。チューリングマシンには再帰の直接的な概念がないため、「再帰なしでチューリングマシンをシミュレートできる」と言っても、実際には何も証明されません。ステートメントを証明するために表示する必要があるのは、チューリングマシンが再帰をシミュレートできることです。あなたがこれを示さないなら、あなたは教会チューリング仮説が再帰(それはそうする)にも当てはまると忠実に仮定しなければならないが、OPはこれに疑問を呈した。
5gon12eder

10
OPの問題は、「できる」ではなく、「最良」、「最も効率的」、またはその他の修飾子です。「チューリング完了」とは、再帰を使用して実行できるすべての処理がループを使用して実行できることを意味します。それが特定の言語実装でそれを行う最良の方法であるかどうかは、まったく異なる質問です。
スティーブンバーナップ

7
「缶」は「ベスト」と同じものではありません。「最高ではない」と「できない」を間違えた場合、麻痺状態になります。何をするにしても、より良い方法がほとんど常にあるからです。
スティーブンバーナップ

31

ループではなく再帰を使用してのみタスクを実行できる場合はありますか?

再帰呼び出しは常にループに変換できます。ループは一時的な状態を格納するために後入れ先出しデータ構造(別名スタック)を使用します。その後、状態を復元します。短い答えは:いいえ、そのようなケースはありません

ただし、「はい」については議論することができます。具体的な簡単な例を見てみましょう:マージソート。データを2つの部分に分割し、それらの部分をマージソートしてから結合する必要があります。パーツでマージソートを行うために実際のプログラミング言語の関数呼び出しを行ってマージソートを行わなくても、実際に関数呼び出しを行うのと同じ機能を実装する必要があります(独自のスタックに状態をプッシュし、異なる開始パラメーターでループを開始し、その後スタックから状態をポップします)。

再帰呼び出しを自分で実装する場合、「プッシュ状態」ステップと「先頭へジャンプ」ステップと「ポップ状態」ステップとして別個に実行するのですか?それに対する答えは、いいえ、それは再帰とは呼ばれず、明示的なスタックを使用した反復と呼ばれます(確立された用語を使用する場合)。


これは、「タスク」の定義にも依存することに注意してください。タスクを並べ替える場合は、多くのアルゴリズムを使用して並べ替えることができますが、その多くは再帰を必要としません。タスクがmerge sortのような特定のアルゴリズムを実装する場合、上記のあいまいさが適用されます。

質問を考えてみましょう、一般的なタスクはありますか?再帰タスクのようなアルゴリズムしかありません。質問の下の@WizardOfMenloのコメントから、アッカーマン関数はその簡単な例です。したがって、別のコンピュータープログラム構造(明示的なスタックを使用した反復)で実装できる場合でも、再帰の概念は独立しています。


2
スタックレスプロセッサのアセンブリを扱うとき、これらの2つの技術は突然1つになります。
ジョシュア

@Joshua Indeed!それは抽象化のレベルの問題です。レベルを1つまたは2つ下にすると、それは単なる論理ゲートです。
ハイド

2
それはまったく正しくありません。反復で再帰をエミュレートするには、ランダムアクセスが可能なスタックが必要です。ランダムアクセスのない単一のスタックと、有限量の直接アクセス可能なメモリがPDAになりますが、これはチューリング完全ではありません。
ジル

@Gilles Old post、なぜランダムアクセススタックが必要なのですか?また、実際のコンピューターはすべて、PDAよりも少ないわけではありません。直接アクセスできるメモリは有限で、スタックはまったくありません(そのメモリを使用する場合を除く)。「実際には再帰を実行できない」と言っている場合、これはあまり実用的な抽象化ではないようです。
ハイド

20

「再帰」をどの程度厳密に定義するかによります。

コールスタックを使用することを厳密に要求する場合(またはプログラムの状態を維持するためのメカニズムを使用する場合)、いつでもそれを使用しないものに置き換えることができます。実際、自然に再帰を頻繁に使用する言語には、末尾呼び出しの最適化を頻繁に使用するコンパイラが含まれる傾向があるため、記述は再帰的ですが、実行は反復的です。

しかし、再帰呼び出しを行い、その再帰呼び出しに再帰呼び出しの結果を使用する場合を考えてみましょう。

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  if (m == 0)
    return  n+1;
  if (n == 0)
    return Ackermann(m - 1, 1);
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

最初の再帰呼び出しを反復するのは簡単です:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
restart:
  if (m == 0)
    return  n+1;
  if (n == 0)
  {
    m--;
    n = 1;
    goto restart;
  }
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

私たちは、その後、クリーンアップは、削除することができますgoto追い払うためにヴェロキラプトルとダイクストラの日陰を:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  while(m != 0)
  {
    if (n == 0)
    {
      m--;
      n = 1;
    }
    else
      return Ackermann(m - 1, Ackermann(m, n - 1));
  }
  return  n+1;
}

しかし、他の再帰呼び出しを削除するには、いくつかの呼び出しの値をスタックに保存する必要があります。

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  Stack<BigInteger> stack = new Stack<BigInteger>();
  stack.Push(m);
  while(stack.Count != 0)
  {
    m = stack.Pop();
    if(m == 0)
      n = n + 1;
    else if(n == 0)
    {
      stack.Push(m - 1);
      n = 1;
    }
    else
    {
      stack.Push(m - 1);
      stack.Push(m);
      --n;
    }
  }
  return n;
}

さて、ソースコードを検討するとき、再帰メソッドを確実に反復メソッドに変えました。

これが何にコンパイルされているかを考慮して、呼び出しスタックを使用するコードを再帰を実装しないコードに変換しました(そして、そうすることで、非常に小さな値でもスタックオーバーフロー例外をスローするコードを単にコードに変換しました)復帰するのに非常に長い時間がかかります(さらに多くの可能な入力に対して実際に復帰する最適化については、アッカーマン関数がスタックからオーバーフローしないようにする方法を参照してください)。

再帰の一般的な実装方法を考慮して、呼び出しスタックを使用するコードを、異なるスタックを使用して保留中の操作を保持するコードに変換しました。したがって、その低レベルで検討すると、それはまだ再帰的であると主張できます。

そして、そのレベルでは、実際に他の方法はありません。したがって、そのメソッドが再帰的であると考えると、それなしではできないことは確かにあります。通常、このようなコードには再帰的なラベルを付けませんが。再帰という用語は、特定のアプローチのセットをカバーし、それらについて話す方法を提供し、そのうちの1つを使用しなくなったため、便利です。

もちろん、これはすべて選択肢があることを前提としています。再帰呼び出しを禁止する言語と、反復に必要なループ構造を持たない言語の両方があります。


コールスタックが制限されているか、コールスタック外の制限のないメモリにアクセスできる場合にのみ、コールスタックを同等のものに置き換えることができます。プッシュスタックオートマトンによって解決可能な重要なクラスの問題があり、それは無制限のコールスタックを持ちますが、そうでなければ有限数の状態しか持つことができません。
supercat

これが最良の答えであり、おそらく唯一の正しい答えです。2番目の例でも再帰的であり、このレベルでは、元の質問に対する答えはnoです。再帰のより広い定義では、アッカーマン関数の再帰を避けることは不可能です。
gerrit

@gerritおよびそれより狭い場合、それは回避されます。最終的には、特定のコードに使用するこの便利なラベルを適用するか、適用しないかの端に来ます。
ジョンハンナ

1
これに賛成票を投じるためにサイトに参加しました。アッカーマン関数は本質的に再帰的です。ループとスタックを使用して再帰構造を実装しても、反復ソリューションにはなりません。再帰をユーザースペースに移動しただけです。
アーロンマクミリン

9

古典的な答えは「いいえ」ですが、「はい」の方が良いと思う理由を詳しく説明できます。


先に進む前に、計算のしやすさと複雑さの観点から、何か邪魔にならないようにしましょう。

  • ループ時に補助スタックを使用できる場合、答えは「いいえ」です。
  • ループ時に余分なデータが許可されていない場合、答えは「はい」です。

さて、ここで、片方の足を練習場に入れ​​、もう一方の足を理論面に入れましょう。


呼び出しスタックは制御構造ですが、手動スタックはデータ構造です。コントロールとデータは同等の概念ではありませんが、計算可能性または複雑性の観点から互いに縮小(または相互に「エミュレート」)できるという意味で同等です。

この区別はいつ重要ですか?実際のツールで作業しているとき。次に例を示します。

N-wayを実装しているとしますmergesortforNセグメントを通過しmergesort、それらを個別に呼び出し、結果をマージするループがある場合があります。

これをOpenMPでどのように並列化できますか?

再帰的な領域では、それは非常に簡単です#pragma omp parallel for。1からNになるループを配置するだけで完了です。反復領域では、これを行うことはできません。スレッドを手動で生成し、適切なデータを手動で渡す必要があります。

他方、#pragma vectorループで動作するが、再帰ではまったく役に立たない他のツール(自動ベクトライザーなど)があります。

ポイントは、2つのパラダイムが数学的に同等であることを証明できるからといって、実際にそれらが同等であることを意味するわけではありません。1つのパラダイムで自動化するのは簡単な問題(ループ並列化など)は、他のパラダイムでは解決するのがはるかに難しい場合があります。

つまり、あるパラダイム用のツールは、他のパラダイムに自動的に変換されません。

そのため、問題を解決するためにツールが必要な場合、そのツールは特定の種類のアプローチでのみ機能する可能性があり、その結果、問題を数学的に証明できる場合でも、別のアプローチで問題を解決することはできませんいずれかの方法で解決されます。


さらに、プッシュダウンオートマトンで解決できる問題のセットは、有限オートマトンで解決できるセット(決定論的または非決定的)よりも大きいが、チューリングマシン。
スーパーキャット

8

理論的推論はさておき、(ハードウェアまたは仮想)マシンの観点から再帰とループがどのように見えるかを見てみましょう。再帰は、いくつかのコードの実行を開始して完了時に戻ることを可能にする制御フロー(シグナルと例外が無視される場合の単純化したビュー)と、他のコードに渡されるデータ(引数)の組み合わせですそれ(結果)。通常、明示的なメモリ管理は含まれませんが、戻りアドレス、引数、結果、および中間ローカルデータを保存するためのスタックメモリの暗黙的な割り当てがあります

ループは、制御フローとローカルデータの組み合わせです。これを再帰と比較すると、この場合のデータ量は固定されていることがわかります。この制限を破る唯一の方法は、必要に応じていつでも割り当て(および解放)できる動的メモリ(ヒープとも呼ばれます)を使用することです。

要約する:

  • 再帰ケース=制御フロー+スタック(+ヒープ)
  • ループケース=制御フロー+ヒープ

制御フロー部分が適度に強力であると仮定すると、唯一の違いは利用可能なメモリタイプにあります。したがって、4つのケースが残ります(表現力は括弧内にリストされています)。

  1. スタックもヒープもありません。再帰と動的構造は不可能です。(再帰=ループ)
  2. スタック、ヒープなし:再帰は問題ありません。動的構造は不可能です。(再帰>ループ)
  3. スタックなし、ヒープ:再帰は不可能で、動的構造は問題ありません。(再帰=ループ)
  4. スタック、ヒープ:再帰および動的構造は問題ありません。(再帰=ループ)

ゲームのルールが少し厳しく、ループの使用が再帰的な実装で許可されていない場合、代わりにこれを取得します。

  1. スタックもヒープもありません。再帰と動的構造は不可能です。(再帰<ループ)
  2. スタック、ヒープなし:再帰は問題ありません。動的構造は不可能です。(再帰>ループ)
  3. スタックなし、ヒープ:再帰は不可能で、動的構造は問題ありません。(再帰<ループ)
  4. スタック、ヒープ:再帰および動的構造は問題ありません。(再帰=ループ)

前のシナリオとの主な違いは、スタックメモリが不足しているため、コード行よりも多くのステップを実行するループなしの再帰ができないことです。


2

はい。再帰を使用して簡単に達成できるが、ループだけでは不可能な一般的なタスクがいくつかあります。

  • スタックオーバーフローの原因。
  • 完全に混乱している初心者プログラマー。
  • 実際にO(n ^ n)である高速な関数を作成します。

3
ループでこれらは本当に簡単です、私は常にそれらを見る。ちょっと、努力すれば、ループさえ必要ありません。たとえ再帰が簡単であっても。
AviD

1
実際には、A(0、n)= n + 1; m> 0の場合、A(m、0)= A(m-1,1); A(m、n)= A(m-1、A(m、n-1))m> 0、n> 0がO(n ^ n)よりも少し速く成長する場合(m = nの場合):)
ジョンドン

1
@JohnDonn少し以上、それは超指数関数的です。n = 3の場合n ^ n ^ n n = 4の場合n ^ n ^ n ^ n ^ nなど。nのn乗n回。
アーロンマクミリン

1

再帰関数とプリミティブな再帰関数には違いがあります。プリミティブな再帰関数は、ループを使用して計算される関数で、ループの実行が開始される前に各ループの最大反復カウントが計算されます。(そして、ここでの「再帰的」は、再帰の使用とは関係ありません)。

プリミティブな再帰関数は、再帰関数よりも厳密に強力ではありません。再帰の最大深度を事前に計算する必要がある再帰を使用する関数を使用した場合も、同じ結果が得られます。


3
これが上記の質問にどのように当てはまるかわかりませんか?その接続をより明確にしてください。
ヤック

1
不正確な「ループ」を、「反復回数が制限されたループ」と「反復回数が無制限のループ」の重要な区別に置き換えます。これは誰もがCS 101から知っていると思います
。– gnasher729

確かに、それでも質問には当てはまりません。問題は、基本的な再帰と再帰ではなく、ループと再帰に関するものです。誰かがC / C ++の違いについて質問し、K&R CとAnsi Cの違いについて答えたと想像してみてください。
ヤック

1

C ++でプログラミングしていて、C ++ 11を使用している場合、再帰を使用して行う必要があることが1つあります。それはconstexpr関数です。ただし、この回答で説明されているように、標準ではこれが512に制限されています。この場合、ループを使用することはできません。その場合、関数はconstexprになり得ませんが、これはc ++ 14で変更されたためです。


0
  • 再帰呼び出しが再帰関数の最初または最後のステートメント(条件チェックを除く)である場合、ループ構造に簡単に変換できます。
  • ただし、関数が再帰呼び出しの前後に他の処理を行う場合、ループに変換するのは面倒です。
  • 関数に複数の再帰呼び出しがある場合、ループだけを使用するコードに変換することはほとんど不可能です。データに追いつくために、いくらかのスタックが必要になります。再帰では、呼び出しスタック自体がデータスタックとして機能します。

ツリーウォーキングには複数の再帰呼び出し(子ごとに1つ)がありますが、明示的なスタックを使用して簡単にループに変換されます。一方、パーサーは変換するのが面倒です。
CodesInChaos

@CodesInChaos編集。
グルシャン

-6

他の質問にも同意します。ループではできない再帰でできることは何もありません。

しかし、私の意見では、再帰は非常に危険です。まず、コードで実際に何が起こっているのかを理解するのが難しい人もいます。第二に、少なくともC ++(Javaではわかりません)の場合、各メソッド呼び出しはメモリの蓄積とメソッドヘッダーの初期化を引き起こすため、各再帰ステップはメモリに影響を与えます。この方法で、スタックを爆破できます。高い入力値でフィボナッチ数の再帰を単に試してください。


2
再帰を伴うフィボナッチ数の単純な再帰的実装は、スタックスペースが不足する前に「時間外」に実行されます。この例にはもっと良い他の問題があると思います。また、多くの問題では、ループバージョンは、スタックではなくヒープ上で、再帰バージョンと同じメモリインパクトを持ちます(プログラミング言語がそれらを区別する場合)。
パエロエベルマン

6
あなただけのループ変数をインクリメントするのを忘れた場合、ループは「非常に危険」ことができます...
H22

2
したがって、実際には、意図的にスタックオーバーフローを生成することは、再帰を使用しないと非常に難しいタスクです。
5gon12eder

私たちをもたらします@ 5gon12eder 方法は、再帰的なアルゴリズムでスタックオーバーフローを避けるためにありますか?-TCOに関与するための文章、またはメモが​​役立つ場合があります。 反復的アプローチ再帰的アプローチは、フィボナッチの2つの異なる再帰的アプローチを扱うため、興味深いものです。

1
再帰でスタックオーバーフローが発生する場合、ほとんどの場合、反復バージョンでハングしていました。少なくとも前者はスタックトレースをスローします。
ジョンハンナ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.