web-dev-qa-db-ja.com

whileループは本質的に再帰ですか?

Whileループが本質的に再帰であるかどうか疑問に思いましたか?

それは、whileループが最後に自分自身を呼び出す関数と見なせるからです。再帰でない場合、違いは何ですか?

38
badbye

ループは非常にnot再帰です。実際、これらは反対メカニズムの主要な例です反復

再帰のポイントは、処理の1つの要素calls自身の別のインスタンスであるということです。ループ制御機構は単にジャンプが開始した時点に戻ります。

コード内をジャンプして別のコードブロックを呼び出すことは、別の操作です。たとえば、ループの先頭にジャンプするとき、ループ制御変数はジャンプ前と同じ値を保持しています。ただし、現在のルーチンの別のインスタンスを呼び出すと、新しいインスタンスにはnew、unrelatedのすべての変数のコピーが含まれます。事実上、1つの変数は最初のレベルの処理で1つの値を持ち、別の値はより低いレベルで持つことができます。

この機能は、多くの再帰アルゴリズムが機能するために重要です。そのため、これらすべての値を追跡する呼び出されたフレームのスタックを管理しなければ、反復によって再帰をエミュレートできません。

118
Kilian Foth

これはあなたの視点に依存します。

計算理論を見ると、反復と再帰は等しく表現可能です。これは、何かを計算する関数を記述できることを意味します。それを再帰的に実行するか反復的に実行するかは関係なく、両方のアプローチを選択できます。 再帰的に計算できるものは何もありません。繰り返し計算することはできません(その逆も同様です)(プログラムの内部動作は異なる場合があります)。

多くのプログラミング言語は、再帰と反復を同じように扱いません。そして、それには正当な理由があります。 通常、再帰は言語/コンパイラが呼び出しスタックを処理することを意味し、反復は自分でスタック処理を行わなければならないことを意味します。

ただし、言語-特に関数型言語-には、ループ(for、while)などがです。確かに再帰のための構文シュガーであり、そのように舞台裏で実装されています。これは関数型言語ではしばしば望ましいことです。関数型言語は通常、そうでなければループの概念を持たないためです。これを追加すると、実用的な理由がほとんどなく、計算がより複雑になります。

だから、いいえ、それらは本質的に同じではありません。それらは同等に表現力があります、つまり、何かを反復的に計算することはできず、再帰的に計算することはできません(その逆も同様です)が、一般的なケースではChurch-Turing論文によると)。

ここでは再帰プログラムについて話していることに注意してください。再帰には他の形式があります。データ構造(例:ツリー)。


実装の観点から見ると、再帰と反復はほとんど同じではありません。再帰は、すべての呼び出しに対して新しいスタックフレームを作成します。再帰のすべてのステップは自己完結型であり、呼び出し先(それ自体)から計算の引数を取得します。

一方、ループは呼び出しフレームを作成しません。それらの場合、コンテキストは各ステップで保持されません。ループの場合、プログラムは、ループ条件が失敗するまでループの先頭にジャンプするだけです。

これは、実世界でかなり根本的な違いを生む可能性があるため、知っておくことが非常に重要です。再帰では、すべての呼び出しでコンテキスト全体を保存する必要があります。反復処理では、メモリ内の変数と保存場所を正確に制御できます。

このように見ると、ほとんどの言語では、反復と再帰は根本的に異なり、異なるプロパティを持つことがすぐにわかります。状況によっては、一部のプロパティが他のプロパティよりも望ましい場合があります。

再帰により、プログラムがよりシンプルでテストしやすくなり、proofになります。再帰を反復に変換すると、通常、コードがより複雑になり、失敗する可能性が高くなります。一方、反復に変換してコールスタックフレームの量を減らすと、必要なメモリを大幅に節約できます。

38
Polygnome

Xが本質的にYであると言うことは、Xを表現していることを念頭に置いた(正式な)システムがある場合にのみ意味があります。whileのセマンティクスをラムダ計算で定義する場合、再帰*;レジスターマシンの観点からそれを定義する場合、おそらくそうしません。

どちらの場合も、whileループが含まれているという理由だけで関数を再帰的に呼び出しても、おそらく理解されないでしょう。

*おそらく間接的にのみですが、たとえばfoldで定義した場合などです。

37
Anton Golov

違いは、暗黙のスタックとセマンティックです。

「最後に自分自身を呼び出す」whileループには、完了時にクロールバックするスタックがありません。最後の反復で、終了時の状態を設定します。

ただし、以前に行われた作業の状態を記憶するこの暗黙のスタックがなければ、再帰は実行できません。

スタックへのアクセスを明示的に与えれば、反復による再帰の問題を解決できることは事実です。しかし、それをそのように行うことは同じではありません。

意味の違いは、再帰的なコードを見ることが、反復的なコードとはまったく異なる方法でアイデアを伝えるという事実に関係しています。反復コードは、一度に1ステップずつ実行します。それは前から来たどんな状態も受け入れ、次の状態を作成するためにのみ機能します。

再帰的なコードは問題をフラクタルに分割します。この小さな部分はその大きな部分のように見えるので、この部分と同じ部分を同じ方法で実行できます。問題について考える別の方法です。それは非常に強力であり、慣れる必要があります。数行で多くのことが言えます。スタックにアクセスできても、whileループからそれを取得することはできません。

12
candied_orange

すべては、用語intrinsicallyの使用にかかっています。プログラミング言語レベルでは、それらは構文的にも意味的にも異なり、パフォーマンスとメモリ使用量もまったく異なります。しかし、理論を深く掘り下げると、それらは相互に定義できるため、理論的には「同じ」です。

本当の質問は:反復(ループ)と再帰を区別するのはいつ意味があり、それを同じものと考えるのがいつ役立つのか?答えは、実際にプログラミングするとき(数学的証明を書くのではなく)、反復と再帰を区別することが重要であるということです。

再帰は、新しいスタックフレーム、つまり各呼び出しのローカル変数の新しいセットを作成します。これはオーバーヘッドがあり、スタック上のスペースを占有します。つまり、十分に深い再帰によりスタックがオーバーフローし、プログラムがクラッシュする可能性があります。一方、反復は既存の変数のみを変更するため、一般的に高速で、一定量のメモリしか使用しません。したがって、これは開発者にとって非常に重要な違いです。

末尾呼び出し再帰を使用する言語(通常は関数型言語)では、コンパイラーは、一定量のメモリーしか使用しないように再帰呼び出しを最適化できる場合があります。これらの言語では、重要な違いは反復と再帰ではなく、非末尾呼び出し再帰バージョンの末尾呼び出し再帰と反復です。

結論:違いを見分けられる必要があります。そうしないと、プログラムがクラッシュします。

8
JacquesB

whileループは再帰の一種です。たとえば、 この質問 への受け入れられた答え。これらは、計算可能性理論におけるμ演算子に対応します(例 here を参照)。

一連の数値、有限コレクション、配列などを反復するforループのすべてのバリエーションは、プリミティブな再帰に対応しています。たとえば、 ここ および ここ 。 C、C++、Javaなどのforループは実際にはwhileループの構文上のシュガーであるため、プリミティブな再帰には対応していません。 Pascal forループは、プリミティブな再帰の例です。

重要な違いは、一般的な再帰(whileループ)は終了しない可能性があるのに対し、プリミティブな再帰は常に終了することです。

[〜#〜]編集[〜#〜]

コメントおよびその他の回答に関するいくつかの説明。 「再帰は、物事がそれ自体またはそのタイプに関して定義されたときに発生します。」 ( wikipedia を参照)。そう、

Whileループは本質的に再帰ですか?

whileループをそれ自体で定義できるので

while p do c := if p then (c; while p do c))

次に、yeswhileループは再帰の形式です。再帰関数は、再帰の別の形式です(再帰定義の別の例)。リストとツリーは、再帰の他の形式です。

多くの回答やコメントで暗黙的に想定されている別の質問は

Whileループと再帰関数は同等ですか?

この質問への答えはnoです:whileループは末尾再帰関数に対応し、ループによってアクセスされる変数暗黙の再帰関数の引数に対応しますが、他の人が指摘したように、非末尾再帰関数は、追加のスタックを使用しないとwhileループでモデル化できません。

したがって、「whileループが再帰の形式」であるという事実は、「一部の再帰関数がwhileループで表現できない」という事実と矛盾しません。

4
Giorgio

末尾呼び出し (または末尾再帰呼び出し)は、「引数付きのgoto」として(まったく追加呼び出しをプッシュせずに)実装されます。 call stack )および一部の関数型言語(特にOcaml)のフレームは、ループの通常の方法です。

したがって、whileループ(それらを使用する言語)は、本体(またはヘッドテスト)への末尾呼び出しで終了していると見なすことができます。

同様に、通常の(末尾呼び出しではない)再帰呼び出しは、ループ(いくつかのスタックを使用)によってシミュレートできます。

継続 および 継続渡しスタイル についてもお読みください。

したがって、「再帰」と「反復」はまったく同じです。

再帰と制限のないwhileループの両方が、計算表現の点で同等であることは事実です。つまり、再帰的に記述されたプログラムは、代わりにループを使用して同等のプログラムに書き直すことができ、その逆も可能です。どちらのアプローチもturing-completeです。つまり、計算可能な関数の計算に使用できます。

プログラミングの点での根本的な違いは、再帰によってコールスタックに格納されたデータを利用できることです。これを説明するために、ループまたは再帰のいずれかを使用して、単一リンクリストの要素を印刷するとします。コード例にはCを使用します。

 typedef struct List List;
 struct List
 {
     List* next;
     int element;
 };

 void print_list_loop(List* l)
 {
     List* it = l;
     while(it != NULL)
     {
          printf("Element: %d\n", it->element);
          it = it->next;
     }
 }

 void print_list_rec(List* l)
 {
      if(l == NULL) return;
      printf("Element: %d\n", l->element);
      print_list_rec(l->next);
 }

シンプルでしょ?次に、少し変更を加えます。リストを逆の順序で印刷します。

再帰的なバリアントの場合、これは元の関数にほとんど取るに足らない修正です。

void print_list_reverse_rec(List* l)
{
    if (l == NULL) return;
    print_list_reverse_rec(l->next);
    printf("Element: %d\n", l->element);
}

ただし、ループ関数については問題があります。私たちのリストは単一にリンクされているため、前方にのみ移動できます。しかし、逆方向に印刷しているため、最後の要素の印刷を開始する必要があります。最後の要素に到達すると、最後から2番目の要素に戻ることができなくなります。

したがって、大量の再トラバースを実行するか、訪問した要素を追跡し、そこから効率的に印刷できる補助データ構造を構築する必要があります。

再帰に関してこの問題がないのはなぜですか?再帰では、すでに補助データ構造が用意されているからです。関数呼び出しスタックです。

再帰により、再帰呼び出しの以前の呼び出しに戻ることができるため、その呼び出しのすべてのローカル変数と状態は変更されていないため、反復の場合にモデル化するのが面倒な柔軟性が得られます。

1
ComicSansMS

ループは、特定のタスク(主に反復)を達成するための再帰の特殊な形式です。複数の言語で同じパフォーマンス[1]のループを再帰的なスタイルで実装できます。そしてSICP [2]では、forループが「syntastic sugar」として記述されていることがわかります。ほとんどの命令型プログラミング言語では、forおよびwhileブロックは、親関数と同じスコープを使用しています。それにもかかわらず、ほとんどの関数型プログラミング言語では、forループもwhileループも存在しません。ループが必要ないためです。

命令型言語がfor/whileループを持っている理由は、状態を変化させることによって状態を処理しているためです。しかし実際には、別の観点から見ると、whileブロック自体を関数として考え、パラメーターを取得して処理し、新しい状態を返します。これは、異なるパラメーターを使用した同じ関数の呼び出しでもあります-ループは再帰と考えることができます。

世界は、可変または不変として定義することもできます。世界を一連のルールとして定義し、すべてのルールと現在の状態をパラメーターとして取る最終的な関数を呼び出し、同じ機能を持つこれらのパラメーターに従って新しい状態を返します(同じ状態で次の状態を生成します)方法)、これは再帰とループであるとも言えます。

次の例では、lifeは関数が「rules」と「state」の2つのパラメーターを受け取り、次回のティックで新しい状態が構築されます。

life rules state = life rules new_state
    where new_state = construct_state_in_time rules state

[1]:末尾呼び出し最適化は、新しい関数を作成する代わりに再帰呼び出しで既存の関数スタックを使用するための関数型プログラミング言語の一般的な最適化です。

[2]:コンピュータプログラムの構造と解釈、MIT。 https://mitpress.mit.edu/books/structure-and-interpretation-computer-programs

0
zeawee