再帰またはwhileループ


123

私はいくつかの開発面接の実践、特に面接で尋ねられた技術的な質問とテストについて読んでいて、ジャンルのことわざに何度かつまずいてきました。「問題はwhileループで解決しました。 「再帰」または「誰でも100行のwhileループでこれを解決できますが、5行の再帰関数でそれを実行できますか?」等

私の質問は、再帰はif / while / for構造より一般的に良いですか?

私はいつも、再帰は優先されるべきではないと考えてきました。なぜなら、それはヒープよりもはるかに小さいスタックメモリに制限されているためです間違っている...



73
再帰については、これは非常に興味深いようです。
dan_waterworth

4
:@dan_waterworthまた、これは役立つだろうgoogle.fr/...を私はいつもそれを逃し、スペルように見える:P
シヴドラゴン

@ShivanDragon私はそう思った^ _ ^昨日投稿したのは非常に適切だ:
ニール

2
私が再帰環境で働いてきた組み込み環境では、せいぜい眉をひそめ、最悪の場合は公にむち打たれます。事実上、スタックスペースが限られているため、違法になります。
フレッド・トムセン

回答:


192

再帰は本質的にループよりも優れているとか悪いというわけではありません。それぞれに利点と欠点があり、それらはプログラミング言語(および実装)にも依存します。

技術的には、反復ループはハードウェアレベルで典型的なコンピューターシステムによりよく適合します。マシンコードレベルでは、ループは単なるテストおよび条件ジャンプです。スタックから。OTOH、再帰の多くの場合(特に反復ループと自明に同等の場合)は、スタックのプッシュ/ポップを回避できるように記述できます。これは、再帰関数呼び出しが、戻る前に関数本体で最後に発生する場合に可能であり、一般に末尾呼び出し最適化(または末尾再帰最適化)として知られています。適切に末尾呼び出しが最適化された再帰関数は、マシンコードレベルでの反復ループとほぼ同等です。

もう1つの考慮事項は、反復ループには破壊的な状態の更新が必要であるため、純粋な(副作用のない)言語セマンティクスと互換性がないことです。これが、Haskellのような純粋な言語にループ構造がまったくない理由であり、他の多くの関数型プログラミング言語にはループが完全に欠けているか、可能な限り回避されています。

しかし、これらの質問がインタビューで非常に多く現れる理由は、それらに答えるためには、変数、関数呼び出し、スコープ、そしてもちろんループと再帰-の多くの重要なプログラミング概念を完全に理解する必要があるからです。テーブルに精神的な柔軟性をもたらし、根本的に異なる2つの角度から問題にアプローチし、同じ概念の異なる症状間を移動できるようにします。

経験と調査によると、変数、ポインター、再帰を理解する能力を持つ人とそうでない人との間には線があります。フレームワーク、API、プログラミング言語、およびそれらのエッジケースを含む、プログラミングの他のほとんどすべては、学習と経験を通じて習得できますが、これらの3つのコアコンセプトの直感を開発できない場合、プログラマーには不向きです。単純な反復ループを再帰バージョンに変換することは、プログラマー以外の人を除外する最も迅速な方法です-かなり経験の浅いプログラマーでも通常15分でそれを行うことができ、言語に依存しない問題です特異性につまずくのではなく、彼らが選んだ言語。

インタビューでこのような質問を受け取った場合、それは良い兆候です。つまり、雇用主は、プログラミングツールのマニュアルを覚えている人ではなく、プログラムできる人を探しているということです。


3
この質問への回答が持つ可能性のある最も重要な側面に触れているため、あなたの答えが一番好きです、それは主要な技術的な部分を説明し、この問題がプログラミング分野にどのように適合するかについてのズームアウトされたビューも提供します。
シヴァンドラゴン

1
また、.next()メソッドを含むイテレータでの再帰呼び出しを優先してループが回避される同期プログラミングのパターンにも気付きました。長時間実行されるコードがCPUに貪欲になりすぎないようにします。
エヴァンプライス

1
反復バージョンには、値のプッシュとポップも含まれます。これを行うには、手動でコードを記述する必要があります。再帰バージョンは、通常、状態を何らかの構造にプッシュすることでこれを手動でシミュレートする必要がある反復アルゴリズムでスタックに状態をプッシュします。最も単純なアルゴリズムのみがこの状態を必要としません。これらの場合、コンパイラは通常、末尾再帰を見つけて反復ソリューションを植え付けることができます。
マーティンヨーク

1
@tdammersあなたが言った研究をどこで読むことができるか教えてもらえますか?「経験と研究は、人々の間に線があることを示唆しています...」これは私にとって非常に興味深いようです。
ユウ松尾

2
忘れてしまったことの1つは、実行の単一スレッドを処理する場合、反復コードのパフォーマンスが向上する傾向がありますが、再帰アルゴリズムは複数のスレッドで実行される傾向があることです。
GordonM

37

場合によります。

また、末尾再帰のサポートにより、末尾再帰ループと繰り返しループが同等になります。つまり、再帰は常にスタックを浪費する必要はありません。

また、明示的なスタックを使用することにより、再帰アルゴリズムを常に反復的に実装できます

最後に、5行のソリューションの方が100行のソリューションよりも常に優れていることに注意してください(実際に同等であると仮定した場合)。


5
いい答え(+1)。「5行の解決策は、おそらく100行の解決策よりも常に優れています」:再帰の利点は簡潔さだけではないと思います。再帰呼び出しを使用すると、異なる反復の値間の機能的な依存関係を明示的にする必要があります。
ジョルジオ

4
より短いソリューションはより良い傾向がありますが、過度に簡潔であるようなものがあります。
dan_waterworth

5
「100行」それがためにかなり困難だと比べ@dan_waterworth 過度に簡潔
ブヨ

4
@Giorgio、不要なコードを削除するか、明示的なものを暗黙的にすることで、プログラムを小さくすることができます。前者に固執している限り、品質は向上します。
dan_waterworth

1
@jk、それは明示的な情報を暗黙的にする別の形式だと思います。変数の用途に関する情報は、明示的な場合はその名前から削除され、暗黙的な使用にプッシュされます。
dan_waterworth

17

プログラミングに関しては、「より良い」の定義について普遍的に合意されているわけではありませんが、「メンテナンス/読み取りがより簡単」という意味に取ります。

再帰には、反復ループ構造よりも表現力があります。whileループは末尾再帰関数と同等であり、再帰関数は末尾再帰である必要がないためです。強力な構成体は、読みにくいものを実行できるため、通常は悪いものです。ただし、再帰を使用すると、可変性を使用せずにループを記述できます。私の考えでは、可変性は再帰よりもはるかに強力です。

したがって、低い表現力から高い表現力まで、ループ構造は次のように積み重ねられます。

  • 不変データを使用する末尾再帰関数、
  • 不変データを使用する再帰関数、
  • 可変データを使用するループでは、
  • 可変データを使用する末尾再帰関数、
  • 可変データを使用する再帰関数、

理想的には、できる限り表現力の低い構成を使用します。もちろん、言語が末尾呼び出しの最適化をサポートしていない場合、ループ構造の選択にも影響する可能性があります。


1
「whileループは末尾再帰関数と同等であり、再帰関数は末尾再帰である必要はありません」:+1。whileループ+スタックを使用して、再帰をシミュレートできます。
ジョルジオ

1
100%同意するかどうかはわかりませんが、確かに興味深い視点なので、+ 1します。
コンラッドルドルフ

+1とすばらしい回答、および一部の言語(またはコンパイラ)がテールコールの最適化を行わないことを言及するため。
シヴァンドラゴン

@Giorgio、「whileループ+スタックによって再帰をシミュレートできます」、これが表現力を言った理由です。計算上、それらは同様に強力です。
dan_waterworth

@dan_waterworth:まさに、あなたの答えで言ったように、再帰をシミュレートするためにwhileループにスタックを追加する必要があるので、再帰だけはwhileループよりも表現力があります。
ジョルジオ

7

多くの場合、再帰はそれほど明白ではありません。明らかではないほど、維持するのが難しくなります。

for(i=0;i<ITER_LIMIT;i++){somefunction(i);}メインフローで記述する場合、ループを記述していることを完全に明確にします。書いたとしてsomefunction(ITER_LIMIT);も、実際に何が起こるかを明確にしないでください。コンテンツのみを見る:このsomefunction(int x)呼び出しsomefunction(x-1)は、実際には反復を使用するループであることを示しています。また、break;反復の途中のどこかに簡単にエスケープ条件を設定することはできません。最後まで渡される条件を追加するか、例外をスローする必要があります。(そして、例外は再び複雑さを追加します...)

本質的に、それが反復と再帰の間の明らかな選択であるならば、直感的なことをしてください。繰り返しが簡単にジョブを実行する場合、2行を保存することは、長期的に生じる可能性のある頭痛の種に値することはめったにありません。

もちろん、98行を節約できる場合は、まったく別の問題です。

再帰が単に完全に適合する状況がありますが、それは実際に珍しいことではありません。ツリー構造、多重リンクネットワーク、独自のタイプを含むことができる構造、多次元のギザギザの配列、基本的に単純なベクトルでも固定次元の配列でもないものの走査。既知の直線経路を移動する場合は、繰り返します。未知に飛び込んだら、再帰してください。

基本的に、somefunction(x-1)レベルごとに複数回それ自体から呼び出される場合、反復を忘れてください。

...再帰によって最適に実行されるタスクの関数を繰り返し記述することは可能ですが、快適ではありません。どこで使用しようとint、あなたはのようなものが必要ですstack<int>。私は一度実践しましたが、実際の目的よりも演習として。そのような課題に直面したら、あなたが表明したような疑念を抱くことはありません。


10
何が明白で何がそれほど明白でないかは、あなたが何に慣れているかにある程度依存します。反復は、CPUの動作方法に近いため(つまり、メモリ使用量が少なく、実行速度が速い)、プログラミング言語でより頻繁に使用されています。しかし、帰納的に考えることに慣れている場合、再帰は同じくらい直感的です。
ジョルジオ

5
「ループキーワードを見ると、それはループであることがわかります。しかし、再帰キーワードはありません。f(x)内のf(x-1)を見るだけで認識できます。」:再帰関数を呼び出すとき再帰的であることを知りたくない。同様に、ループを含む関数を呼び出すとき、ループが含まれていることを知りたくありません。
ジョルジオ

3
@SF:はい。ただし、関数の本体を見ると、これを見ることができます。ループの場合はループが表示され、再帰の場合は関数がそれ自体を呼び出すことがわかります。
ジョルジオ

5
@SF:循環的な推論に少し似ているように見えます:「私の直感でループである場合、それはループです。」リストを横断し、リストの各メンバーに関数を適用することが直感的に明らかな場合でもmap、再帰関数として定義できます(たとえばhaskell.org/tutorial/functions.htmlを参照)。
ジョルジオ

5
@SF mapはキーワードではなく、通常の機能ですが、それは少し無関係です。関数型プログラマが再帰を使用するとき、それは通常、アクションのシーケンスを実行するためではなく、解決される問題を関数と引数のリストとして表現できるためです。問題は、別の関数と引数の別のリストに縮小できます。最終的に、あなたは些細なことで解決できる問題を抱えています。
dan_waterworth

6

いつものように、実際にはケース間で広く等しく、ユースケース内で互いに等しくない追加の要因があるため、これは一般に答えられません。ここにいくつかのプレッシャーがあります。

  • 一般に、短くてエレガントなコードは、長くて複雑なコードよりも優れています。
  • ただし、開発者ベースが再帰に精通しておらず、学習したくない/学習できない場合、最後の点は多少無効になります。ポジティブではなく、わずかにネガティブになることさえあります。
  • 再帰は、効率のために悪いことができるならば、あなたが実際に深くネストされた呼び出しが必要になりますし、あなたが末尾再帰(またはご使用の環境が、末尾再帰を最適化することはできません)を使用することはできません。
  • 中間結果を適切にキャッシュできない場合、多くの場合、再帰は不適切です。たとえば、ツリーの再帰を使用してフィボナッチ数を計算する一般的な例は、キャッシュしないとひどく悪くなります。キャッシュを使用すると、シンプルで高速、エレガントで、全体的に素晴らしいものになります。
  • 再帰は、一部のケースには適用されず、他のケースでは反復と同じくらい良好であり、他のケースでは絶対に必要です。通常、ビジネスルールの長いチェーンを駆け抜けることは、再帰によってまったく助けられません。データストリームの反復処理は、再帰を使用すると便利です。多次元の動的データ構造(迷路、オブジェクトツリーなど)を反復処理することは、再帰的、明示的または暗黙的でないとほとんど不可能です。これらの場合、明示的な再帰は暗黙的よりもはるかに優れていることに注意してください-怖いRワードを避けるために、誰かが言語内でアドホックで不完全なバグのあるスタックを実装したコードを読むことほど苦痛はありません。

再帰に関連してキャッシュすることはどういう意味ですか?
ジョルジオ

@Giorgioはおそらくメモ化
jk。

うん 環境がテールコールを最適化しない場合は、より良い環境を見つける必要があり、開発者が再帰に慣れていない場合は、より良い開発者を見つける必要があります。いくつかの基準があります!
CAマッキャン

1

再帰とは、関数の呼び出しを繰り返すことです。ループとは、メモリ内の場所へのジャンプを繰り返すことです。

スタックオーバーフローについても言及する必要があります-http://en.wikipedia.org/wiki/Stack_overflow


1
再帰とは、その定義がそれ自体の呼び出しを伴う関数を意味します。
ハードマス

1
単純な定義は正確に100%正確ではありませんが、スタックオーバーフローについて言及したのはあなただけです。
Qix

1

それは本当に利便性または要件に依存します:

プログラミング言語Pythonを使用する場合、再帰をサポートしますが、デフォルトでは再帰の深さに制限があります(1000)。制限を超えると、エラーまたは例外が発生します。この制限は変更できますが、変更すると、言語に異常な状況が発生する可能性があります。

この時点で(再帰の深さを超える呼び出しの数)、ループ構造を優先する必要があります。つまり、スタックサイズが十分でない場合は、ループ構造を優先する必要があります。


3
ここで彼は(異なる言語が明確な戦術的なアプローチを取るという概念をサポートしている)Pythonで末尾再帰の最適化を望んでいない理由として、グイド・ヴァンロッサムのブログです。
ハードマス

-1

戦略設計パターンを使用します。

  • 再帰はきれいです
  • ループは(ほぼ間違いなく)効率的です

負荷(および/または他の条件)に応じて、いずれかを選択します。


5
待って、何?戦略パターンはここにどのように適合しますか?そして、2番目の文は空のフレーズのように聞こえます。
コンラッドルドルフ

@KonradRudolph私は再帰に行きます。非常に大きなデータセットの場合、ループに切り替えます。それが私が意味したことです。はっきりしない場合は謝罪します。
プラビンソナウェン

3
あ。さて、これが「戦略設計パターン」と呼ばれるかどうかはまだわかりませんが、これは非常に固定された意味を持ち、比use的に使用します。しかし、今では少なくともあなたがどこへ行くのかわかります。
コンラッドルドルフ

@KonradRudolphは重要な教訓を学びました。あなたが言いたいことを深く説明してください..ありがとう..それは助けた..)
プラビンソナウェン

2
@Pravin Sonawane:末尾再帰の最適化を使用できる場合は、巨大なデータセットでも再帰を使用できます。
ジョルジオ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.