私はいくつかの開発面接の実践について、特に面接で尋ねられる技術的な質問とテストについて読んでいて、ジャンルのことわざにかなり何度もつまずきました。「さて、whileループで問題を解決しました。再帰」または「誰もが100行のwhileループでこれを解決できますが、5行の再帰関数で実行できますか?」等.
私の質問は、再帰は一般的にif/while/for構文よりも優れているかどうかです。
正直に言って、再帰はスタックよりもはるかに小さいスタックメモリに制限されているため、再帰は優先されないと考えていました。また、パフォーマンスの観点から、多数の関数/メソッド呼び出しを実行することは最適ではありませんが、間違っている...
再帰はループよりも本質的に良くも悪くもありません-それぞれループには長所と短所があり、それらはプログラミング言語(および実装)にも依存します。
技術的には、反復ループはハードウェアレベルで典型的なコンピューターシステムによりよく適合します。マシンコードレベルでは、ループは単なるテストおよび条件付きジャンプですが、再帰(単純に実装)はスタックフレームのプッシュ、ジャンプ、戻り、ポップバックを含みますスタックから。 OTOH、再帰の多くのケース(特に反復ループと自明に同等のケース)を記述して、スタックのプッシュ/ポップを回避できるようにすることができます。これは、再帰的な関数呼び出しが戻る前に関数本体で発生する最後のものであり、一般的に末尾呼び出し最適化(または末尾再帰最適化)として知られている場合に可能です。 。適切に末尾呼び出しが最適化された再帰関数は、ほとんどの場合、マシンコードレベルの反復ループと同等です。
もう1つの考慮事項は、反復ループには破壊的な状態の更新が必要なため、純粋な(副作用のない)言語のセマンティクスと互換性がないことです。これが、Haskellのような純粋な言語にループ構造がまったくない理由であり、他の多くの関数型プログラミング言語では、ループ構造が完全に欠落しているか、可能な限り回避されています。
しかし、これらの質問がインタビューでそれほど多く現れる理由は、それらに答えるために、変数、関数呼び出し、スコープ、そしてもちろんループと再帰-の多くの重要なプログラミング概念を完全に理解する必要があるためです。テーブルに精神的な柔軟性をもたらし、2つの根本的に異なる角度から問題に取り組み、同じ概念の異なる症状の間を移動できるようにします。
経験と研究によると、変数、ポインター、および再帰を理解する能力を持つ人々とそうでない人々の間には線引きがあることが示唆されています。フレームワーク、API、プログラミング言語、およびそれらのEdgeケースを含む、プログラミングの他のほとんどすべては、学習と経験を通じて習得できますが、これら3つのコアコンセプトの直感を開発できない場合、プログラマーとしての資格はありません。単純な反復ループを再帰バージョンに変換することは、プログラマーではない人を除外する最も速い方法についてです。経験の浅いプログラマーでも、通常15分で実行でき、言語にとらわれない問題なので、候補者は選択できます。特異性についてつまずくのではなく、選択した言語。
インタビューでこのような質問が出た場合、それは良い兆候です。雇用主候補は、プログラミングツールのマニュアルを覚えている人ではなく、プログラミングできる人を探していることを意味します。
場合によります。
tail recursion のサポートにより、tailが再帰的になり、反復ループが同等になることにも注意してください。つまり、再帰は常にスタックを無駄にする必要はありません。
また、再帰的アルゴリズムは常に繰り返し実装できます 明示的なスタックを使用することにより 。
最後に、5行のソリューションの方が100行のソリューションよりも常に優れていることに注意してください(実際には同等であると想定しています)。
プログラミングに関しては、「より良い」の定義について普遍的に合意されたものはありませんが、「保守/読み取りが容易」であることを意味します。
再帰は反復ループ構造よりも表現力があります。これは、whileループが末尾再帰関数と同等であり、再帰関数が末尾再帰である必要がないためです。強力な構成体は、読みにくいことを可能にするため、通常は悪いことです。ただし、再帰は可変性を使用せずにループを作成する機能を提供します。私の考えでは、可変性は再帰よりもはるかに強力です。
したがって、低い表現力から高い表現力まで、ループ構造は次のように重なります。
理想的には、できる限り表現の少ない構成を使用します。もちろん、言語が末尾呼び出しの最適化をサポートしていない場合は、ループ構造の選択にも影響する可能性があります。
多くの場合、再帰はそれほど明白ではありません。あまり目立たないものを維持することは困難です。
メインフローで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>
_のようなものが必要です。私はそれを1回行いましたが、実際の目的というよりは練習として行いました。あなたがそのような課題に直面したら、あなたが表明したような疑いを抱くことはないでしょう。
いつものように、実際にはケース間で等しくなく、ユースケース内では互いに等しくない追加の要素があるため、これは一般に答えられません。ここにいくつかのプレッシャーがあります。
それは本当に利便性や要件に依存します:
プログラミング言語 Python を使用すると、再帰がサポートされますが、デフォルトでは再帰の深さ(1000)に制限があります。制限を超えると、エラーまたは例外が発生します。この制限は変更できますが、変更すると、言語に異常が発生する可能性があります。
現時点では(再帰の深さよりも呼び出しの数が多い)、ループ構造を優先する必要があります。つまり、スタックサイズが十分でない場合は、ループ構造を優先する必要があります。
再帰は関数の呼び出しを繰り返すことであり、ループはジャンプを繰り返してメモリに配置することです。
スタックオーバーフローについても言及する必要があります- http://en.wikipedia.org/wiki/Stack_overflow