インタビューで、問題を解決するために再帰を使用する場合があります(たとえば、無限精度整数に1
を追加するなど)、または問題が再帰の使用に適している場合。場合によっては、問題解決のために再帰を頻繁に使用することが原因である可能性があるため、あまり考えずに、再帰を使用して問題を解決します。
しかし、問題を解決するために再帰を使用することが適切であると判断する前に、どのような考慮事項がありますか?
私が持っていたいくつかの考え:
毎回半分にされるデータで再帰を使用する場合、16 GBのRAM、または8 TBのハードドライブに収まるすべてのデータは、42レベルの深さの再帰で処理できるため、再帰を使用しても問題はないようです。 (したがって、スタックオーバーフローはありません(一部の環境では、スタックは4000レベルの深さで42をはるかに超える可能性がありますが、同時に、各コールスタックがより多くのメモリを占有するため、ローカル変数の数にも依存しますローカル変数が多く、スタックオーバーフローを決定するのはレベルではなくメモリサイズである場合)。
純粋な再帰を使用してフィボナッチ数を計算する場合、中間結果をキャッシュしない限り、時間の複雑さを心配する必要があります。
そして、1
を無限精度整数に追加するのはどうですか?たぶんそれは議論の余地があります、あなたが3000桁または4000桁の長さで、それがスタックオーバーフローを引き起こす可能性があるほど大きい数で作業するでしょうか?私はそれを考えていませんでしたが、おそらく答えは「いいえ」です。再帰を使用すべきではありませんが、単純なループを使用してください。アプリケーションによっては、いくつかを確認するために、実際には数が4000桁である必要があるためです。数が素数であるかどうかなど、数のプロパティ。
最終的な問題は、再帰を使用して問題を解決する前に、どのような考慮事項がありますか?
考慮事項の1つは、アルゴリズムが抽象的なソリューションであるのか、それとも実用的な実行可能ソリューションであるのかです。前者の場合、探している属性は正確であり、対象読者にとって理解しやすいことです。1。後者の場合、パフォーマンスも問題になります。これらの考慮事項は、選択に影響する場合があります。
2番目の考慮事項(実用的なソリューションの場合)は、使用しているプログラミング言語(厳密にはその実装)が末尾呼び出しの排除を行うかどうかです。末尾呼び出しの除去がない場合、再帰は反復よりも遅く、深い再帰はスタックオーバーフローの問題を引き起こす可能性があります。
(正しい)再帰的解は変換で同等の非再帰的解に変換できるため、必ずしも2つの方法を厳密に選択する必要はありません。
最後に、再帰的または非再帰的な定式化の選択は、アルゴリズムに関するプロパティを(正式な意味で)証明する必要があるために動機付けられることがあります。再帰的定式化により、帰納法による証明がより直接的に可能になります。
1-このincludesターゲットオーディエンスのような考慮事項...そして、これにはプログラマが実用的なコードを読み取ることが含まれる可能性があります...あるスタイルのソリューションを他のスタイルよりも「より自然な」ものと見なします。 「自然」の概念は、プログラミングやアルゴリズムをどのように学んだかに応じて、人によって異なります。 (私は、客観的な用語で「自然さ」を定義するために再帰を使用する(または使用しない)ことを決定するためのprimary基準として「自然さ」を提案する人に挑戦します。つまり、どのようにそれを測定しますか?)
C/C++プログラマーとして、私の最大の考慮事項はパフォーマンスです。私の決定プロセスは次のようなものです:
呼び出しスタックの最大の深さはどれくらいですか?深すぎる場合は、再帰を取り除きます。浅い場合は、2に進みます。
この機能は私のプログラムのボトルネックになりそうですか?はいの場合は3に進みます。いいえの場合は、再帰を続けます。不明な場合は、プロファイラーを実行します。
再帰的な関数呼び出しに費やされるCPU時間の割合はどれくらいですか?関数呼び出しにかかる時間が他の関数本体よりも大幅に短い場合は、再帰を使用しても問題ありません。
しかし、問題を解決するために再帰を使用することが適切であると判断する前に、どのような考慮事項がありますか?
Schemeで関数を書くとき、あまり考えずに末尾再帰関数を書くのが自然だと思います。
C++で関数を書くとき、私は再帰関数を使用する前に議論していることに気づきます。私が自問する質問は次のとおりです。
反復アルゴリズムを使用して計算を実行できますか?はいの場合、反復的なアプローチを使用します。
モデルのサイズによって再帰の深さが大きくなりますか?私は最近、モデルのサイズが原因で再帰の深さがほぼ13000に増えたケースに遭遇しました。関数を変換して、急いで反復アルゴリズムを使用する必要がありました。
このため、再帰関数を使用してツリートラバーサルアルゴリズムを作成することはお勧めしません。ランタイム環境にとってツリーが深くなりすぎるのはいつかわかりません。
反復アルゴリズムを使用すると、関数が複雑になりすぎますか?はいの場合、再帰関数を使用します。反復的なアプローチを使用してqsort
を記述したことはありませんが、再帰関数を使用する方が自然であると感じています。
フィボナッチ数列の場合、単純な「再帰」はまったく愚かです。これは、同じサブ問題が何度も解決されるためです。
再帰が非常に効率的なフィボナッチ数には自明なバリエーションがあります。n≥1の数値を指定すると、fib(n)とfib(n-1)の両方を計算します。したがって、2の結果を返す関数が必要です。この関数をfib2と呼びましょう。
実装は非常に簡単です。
function fib2 (n) -> (fibn, fibnm1) {
if n ≤ 1 { return (1, 1) }
let (fibn, fibnm1) = fib2 (n-1)
return (fibn + fibnm1, fibn)
}