両方で同じ目的を果たすことができるアルゴリズムで、再帰ではなくループを使用した場合、またはその逆の場合、パフォーマンスに影響がありますか?例:指定された文字列が回文かどうかを確認します。多くのプログラマーが再帰を使用して、単純な反復アルゴリズムがうまく機能する時期を自慢する方法を見てきました。コンパイラーは、何を使用するかを決定する上で重要な役割を果たしますか?
両方で同じ目的を果たすことができるアルゴリズムで、再帰ではなくループを使用した場合、またはその逆の場合、パフォーマンスに影響がありますか?例:指定された文字列が回文かどうかを確認します。多くのプログラマーが再帰を使用して、単純な反復アルゴリズムがうまく機能する時期を自慢する方法を見てきました。コンパイラーは、何を使用するかを決定する上で重要な役割を果たしますか?
回答:
再帰関数が末尾再帰である(最後の行が再帰呼び出しである)かどうかによっては、再帰がより高価になる可能性があります。テール再帰はコンパイラーによって認識され、その反復対応に最適化されます(コード内の簡潔で明確な実装を維持しながら)。
私は、アルゴリズムを最も意味のある方法で記述し、数か月または数年でコードを維持する必要がある貧しい吸盤(自分でも他の誰かでも)にとって最もわかりやすい方法で記述します。パフォーマンスの問題が発生した場合は、コードのプロファイルを作成してから、反復的な実装に移って最適化を検討してください。メモ化と動的プログラミングを調べてみてください。
tail recursion is optimized by compilers
しかし、すべてのコンパイラは、尾再帰をサポートしてい...
ループにより、プログラムのパフォーマンスが向上する場合があります。再帰により、プログラマーのパフォーマンスが向上する場合があります。あなたの状況でどちらがより重要かを選択してください!
再帰と反復の比較は、プラスドライバとマイナスドライバを比較するようなものです。ほとんどの場合、平頭のフィリップスヘッドネジは外すことができますが、そのネジ用に設計されたドライバーを使用すると簡単です。
一部のアルゴリズムは、設計方法が原因で再帰に適しています(フィボナッチ数列、構造のようなツリーのトラバースなど)。再帰により、アルゴリズムはより簡潔で理解しやすくなります(したがって、共有および再利用が可能になります)。
また、一部の再帰アルゴリズムは「遅延評価」を使用しており、反復兄弟よりも効率的です。これは、ループが実行されるたびではなく、必要なときにのみ高価な計算を実行することを意味します。
それはあなたが始めるのに十分なはずです。私もあなたのためにいくつかの記事と例を掘り下げます。
リンク1: HaskelとPHP(再帰と反復)
以下は、プログラマーがPHPを使用して大きなデータセットを処理しなければならない例です。彼はHaskelで再帰を使用して処理するのがいかに簡単であったかを示していますが、PHPには同じ方法を実行する簡単な方法がなかったため、反復を使用して結果を取得する必要がありました。
http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html
リンク2:再帰をマスターする
再帰の評判の悪さのほとんどは、命令型言語の高コストと非効率性に起因しています。この記事の著者は、再帰アルゴリズムを最適化して、アルゴリズムをより高速かつ効率的にする方法について説明しています。また、従来のループを再帰関数に変換する方法と、末尾再帰を使用する利点についても説明します。彼の締めくくりの言葉は、私が考える私の重要なポイントのいくつかを本当に要約しました:
「再帰的プログラミングは、保守可能で論理的に一貫性のある方法でコードを編成するより良い方法をプログラマーに提供します。」
リンク3:再帰はループよりも速いですか?(回答)
これは、あなたに似たstackoverflow質問の回答へのリンクです。いずれかの再帰に関連したベンチマークのロットまたはループであることを、著者のポイントアウト非常に言語固有。命令型言語は通常、ループを使用すると高速になり、再帰を使用すると低速になり、関数型言語ではその逆になります。このリンクからの主なポイントは、言語にとらわれない/状況の盲目的な感覚で質問に答えることは非常に難しいということです。
通常、パフォーマンスの低下は他の方向にあると予想されます。再帰呼び出しは、追加のスタックフレームの構築につながる可能性があります。これに対するペナルティはさまざまです。また、Pythonなどの一部の言語では(より正確には、一部の言語の一部の実装では...)、ツリーデータ構造で最大値を見つけるなど、再帰的に指定する可能性のあるタスクのスタック制限にかなり容易に遭遇する可能性があります。これらの場合、あなたは本当にループを使いたいです。
末尾の再帰を最適化するコンパイラーがあると仮定すると、優れた再帰関数を作成すると、パフォーマンスのペナルティがいくらか軽減されます(また、関数が本当に末尾再帰であることを確認してください---これは多くの人が間違いを犯していることの1つです)オン。)
「エッジ」ケース(ハイパフォーマンスコンピューティング、非常に大きな再帰の深さなど)とは別に、意図を最も明確に表現し、適切に設計され、保守可能なアプローチを採用することをお勧めします。ニーズを特定してから最適化してください。
再帰は、反復よりも、複数の小さな断片に分解できる問題の方が優れています。
たとえば、再帰的なフィボナッチアルゴリズムを作成するには、fib(n)をfib(n-1)とfib(n-2)に分解し、両方の部分を計算します。反復では、単一の関数を何度も繰り返すことができます。
しかし、フィボナッチは実際には壊れた例であり、反復は実際にはより効率的だと思います。fib(n)= fib(n-1)+ fib(n-2)およびfib(n-1)= fib(n-2)+ fib(n-3)であることに注意してください。fib(n-1)は2回計算されます!
より良い例は、ツリーの再帰的アルゴリズムです。親ノードの分析の問題は、各子ノードの分析に関する複数の小さな問題に分類できます。フィボナッチの例とは異なり、小さな問題は互いに独立しています。
そうそう-再帰は、複数の、より小さく、独立した、同様の問題に分解できる問題の反復よりも優れています。
再帰を使用すると、パフォーマンスが低下します。これは、任意の言語でメソッドを呼び出すと、多くの準備が必要になるためです。呼び出しコードは、戻りアドレス、呼び出しパラメーター、プロセッサーレジスタなどの他のコンテキスト情報をどこかに保存し、戻り時に呼び出されたメソッドは戻り値をポストします。戻り値は呼び出し元によって取得され、以前に保存されたコンテキスト情報が復元されます。反復アプローチと再帰アプローチのパフォーマンスの違いは、これらの操作にかかる時間にあります。
実装の観点から見ると、呼び出しコンテキストの処理にかかる時間が、メソッドの実行にかかる時間と同等である場合に、違いに気付くようになります。再帰的メソッドの実行に呼び出し側のコンテキスト管理部分よりも時間がかかる場合は、コードが一般的に読みやすく、理解しやすく、パフォーマンスの低下に気付かないため、再帰的な方法を使用してください。それ以外の場合は、効率上の理由から繰り返します。
Javaの末尾再帰は現在最適化されていないと思います。LtUと関連リンクに関するこのディスカッション全体に詳細が散りばめられています。これは次のバージョン7の機能である可能性がありますが、スタック検査と組み合わせると特定のフレームが欠落するため、明らかに特定の問題が発生します。スタック検査は、Java 2以降、きめ細かなセキュリティモデルの実装に使用されています。
再帰は、状況によっては非常に役立ちます。たとえば、階乗を見つけるためのコードを考えます
int factorial ( int input )
{
int x, fact = 1;
for ( x = input; x > 1; x--)
fact *= x;
return fact;
}
再帰関数を使用して考えてみましょう
int factorial ( int input )
{
if (input == 0)
{
return 1;
}
return input * factorial(input - 1);
}
この2つを観察すると、再帰がわかりやすいことがわかります。ただし、注意して使用しないと、エラーが発生しやすくなります。もしを見逃したとするとif (input == 0)
、コードはしばらくの間実行され、通常はスタックオーバーフローで終了します。
foldl (*) 1 [1..n]
ています。それだけです。
多くの場合、キャッシュが原因で再帰が速くなり、パフォーマンスが向上します。たとえば、これは従来のマージルーチンを使用したマージソートの反復バージョンです。キャッシングによりパフォーマンスが向上するため、再帰的な実装よりも実行が遅くなります。
public static void sort(Comparable[] a)
{
int N = a.length;
aux = new Comparable[N];
for (int sz = 1; sz < N; sz = sz+sz)
for (int lo = 0; lo < N-sz; lo += sz+sz)
merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(a, aux, lo, mid);
sort(a, aux, mid+1, hi);
merge(a, aux, lo, mid, hi);
}
PS-これは、Courseraで提示されたアルゴリズムのコースについてKevin Wayne教授(プリンストン大学)が語ったことです。
再帰を使用すると、「反復」ごとに関数呼び出しのコストが発生しますが、ループでは、通常支払う唯一のものは増分/減分です。したがって、ループのコードが再帰的ソリューションのコードよりもはるかに複雑でない場合、ループは通常、再帰よりも優れています。
リストを繰り返し処理しているだけの場合は、繰り返し処理してください。
他のいくつかの回答では、(深さ優先)ツリートラバーサルについて言及しています。非常に一般的なデータ構造に対して行うことは非常に一般的なことなので、これは本当に素晴らしい例です。この問題では、再帰は非常に直感的です。
ここで「検索」メソッドを確認してください:http : //penguin.ewu.edu/cscd300/Topic/BSTintro/index.html
再帰は、可能な反復の定義よりも単純です(したがって、より基本的です)。1 組のコンビネーターのみでチューリング完全なシステムを定義できます(そうです、再帰自体もそのようなシステムの派生概念です)。ラムダ計算は、同様に強力な基本システムであり、再帰関数を備えています。しかし、イテレーションを適切に定義したい場合は、最初にもっと多くのプリミティブが必要になります。
コードに関して-いいえ、ほとんどのデータ構造は再帰的であるため、再帰的コードは純粋に反復的なコードよりも実際に理解と保守がはるかに簡単です。もちろん、それを正しく行うためには、少なくとも高次の関数とクロージャーをサポートする言語が必要です。すべての標準のコンビネーターとイテレーターをきちんと取得するためです。もちろん、FC ++などのハードコアユーザーでない限り、複雑な再帰的ソリューションは少し見苦しく見えることがあります。
「再帰の深さ」に依存します。これは、関数呼び出しのオーバーヘッドが総実行時間にどの程度影響するかによって異なります。
たとえば、次の理由により、古典的な階乗を再帰的に計算することは非常に非効率的です。-データがオーバーフローするリスク-スタックがオーバーフローするリスク-関数呼び出しのオーバーヘッドが実行時間の80%を占める
チェスのゲームで位置分析のための最小-最大アルゴリズムを開発している間、後続のN動作を分析することで、「分析の深さ」を再帰的に実装できます(私が^ _ ^を実行しているため)
再帰?どこから始めれば、wikiは「アイテムを自己相似的な方法で繰り返すプロセスです」と通知します
私がCをやっていた当時、C ++の再帰は "Tail recursion"のようなものでした。また、多くのソートアルゴリズムは再帰を使用しています。クイックソートの例:http : //alienryderflex.com/quicksort/
再帰は、特定の問題に役立つ他のアルゴリズムと同様です。多分あなたはすぐにまたは頻繁に用途を見つけられないかもしれませんが、それが利用可能であることにうれしいでしょう問題があります。
C ++では、再帰関数がテンプレート化されたものである場合、すべての型の推定と関数のインスタンス化がコンパイル時に行われるため、コンパイラーはそれを最適化する機会が多くなります。最新のコンパイラは、可能であれば関数をインライン化することもできます。したがって、-O3
または-O2
で最適化フラグを使用する場合g++
、再帰は反復よりも高速になる可能性があります。反復コードでは、コンパイラーは既に最適化された状態にあるため(十分に記述されている場合)、最適化する機会が少なくなります。
私の場合、私は再帰的および反復的な方法の両方で、Armadilloマトリックスオブジェクトを使用して二乗することにより、マトリックスのべき乗を実装しようとしていました。アルゴリズムはここにあります... https://en.wikipedia.org/wiki/Exponentiation_by_squaring。私の関数はテンプレート化されており1,000,000
12x12
、累乗された行列を計算しました10
。次の結果が得られました。
iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec
iterative + No-optimisation flag -> 2.83.. sec
recursive + No-optimisation flag -> 4.15.. sec
これらの結果は、c ++ 11フラグ(-std=c++11
)付きのgcc-4.8 とIntel mkl付きのArmadillo 6.1を使用して得られました。Intelコンパイラも同様の結果を示します。
再帰には、再帰を使用して作成するアルゴリズムにO(n)空間の複雑さがあるという欠点があります。反復的なアプローチはO(1)のスペースの複雑さを持っていますが、これは再帰よりも反復を使用する利点です。では、なぜ再帰を使用するのでしょうか。
下記参照。
反復を使用して同じアルゴリズムを記述する方が少し難しい一方で、再帰を使用してアルゴリズムを記述する方が簡単な場合があります。この場合、反復アプローチに従うことを選択した場合は、自分でスタックを処理する必要があります。
反復がアトミックであり、新しいスタックフレームをプッシュよりも桁違いに高価な場合や、新しいスレッドを作成し、あなたが複数のコアを持っているし、あなたのランタイム環境は、それらのすべてを使用することができると組み合わせると、その後、再帰的なアプローチは、巨大なパフォーマンス向上をもたらす可能性マルチスレッド。反復の平均数が予測できない場合は、スレッドの割り当てを制御し、プロセスが多すぎるスレッドを作成してシステムを占有するのを防ぐスレッドプールを使用することをお勧めします。
たとえば、一部の言語では、再帰的なマルチスレッドのマージソート実装があります。
ただし、繰り返しになりますが、マルチスレッドは再帰ではなくループで使用できるため、この組み合わせがどの程度うまく機能するかは、OSやそのスレッド割り当てメカニズムなど、より多くの要因に依存します。
私の知る限り、Perlは末尾再帰呼び出しを最適化していませんが、偽造することはできます。
sub f{
my($l,$r) = @_;
if( $l >= $r ){
return $l;
} else {
# return f( $l+1, $r );
@_ = ( $l+1, $r );
goto &f;
}
}
最初に呼び出されたとき、スタックにスペースを割り当てます。次に、引数を変更し、スタックに何も追加せずにサブルーチンを再起動します。したがって、それは自分自身を呼び出したことがないふりをして、それを反復プロセスに変更します。
" my @_;
"または " local @_;
" がないことに注意してください。そうした場合、機能しなくなります。
Chrome 45.0.2454.85 mだけを使用すると、再帰はかなり速くなるようです。
これがコードです:
(function recursionVsForLoop(global) {
"use strict";
// Perf test
function perfTest() {}
perfTest.prototype.do = function(ns, fn) {
console.time(ns);
fn();
console.timeEnd(ns);
};
// Recursion method
(function recur() {
var count = 0;
global.recurFn = function recurFn(fn, cycles) {
fn();
count = count + 1;
if (count !== cycles) recurFn(fn, cycles);
};
})();
// Looped method
function loopFn(fn, cycles) {
for (var i = 0; i < cycles; i++) {
fn();
}
}
// Tests
var curTest = new perfTest(),
testsToRun = 100;
curTest.do('recursion', function() {
recurFn(function() {
console.log('a recur run.');
}, testsToRun);
});
curTest.do('loop', function() {
loopFn(function() {
console.log('a loop run.');
}, testsToRun);
});
})(window);
結果
//標準のforループを使用して100回実行
ループ実行用に100倍。完了までの時間:7.683ms
//末尾再帰を伴う関数型再帰アプローチを使用して100回実行
100x再帰実行。完了までの時間:4.841ms
以下のスクリーンショットでは、再帰は、テストごとに300サイクルで実行すると、より大きなマージンで再び勝利します。
これらのアプローチには別の違いがあることがわかりました。シンプルで重要ではないように見えますが、インタビューの準備をしているときに非常に重要な役割を果たし、このテーマが浮上するので、よく見てください。
要約すると、1)反復ポストオーダートラバーサルは容易ではありません。これにより、DFTがより複雑になります。2)再帰により、サイクルチェックが容易になります。
詳細:
再帰的なケースでは、トラバーサルの前後を簡単に作成できます。
かなり標準的な質問を想像してください:「タスクが他のタスクに依存している場合、タスク5を実行するために実行する必要があるすべてのタスクを出力します」
例:
//key-task, value-list of tasks the key task depends on
//"adjacency map":
Map<Integer, List<Integer>> tasksMap = new HashMap<>();
tasksMap.put(0, new ArrayList<>());
tasksMap.put(1, new ArrayList<>());
List<Integer> t2 = new ArrayList<>();
t2.add(0);
t2.add(1);
tasksMap.put(2, t2);
List<Integer> t3 = new ArrayList<>();
t3.add(2);
t3.add(10);
tasksMap.put(3, t3);
List<Integer> t4 = new ArrayList<>();
t4.add(3);
tasksMap.put(4, t4);
List<Integer> t5 = new ArrayList<>();
t5.add(3);
tasksMap.put(5, t5);
tasksMap.put(6, new ArrayList<>());
tasksMap.put(7, new ArrayList<>());
List<Integer> t8 = new ArrayList<>();
t8.add(5);
tasksMap.put(8, t8);
List<Integer> t9 = new ArrayList<>();
t9.add(4);
tasksMap.put(9, t9);
tasksMap.put(10, new ArrayList<>());
//task to analyze:
int task = 5;
List<Integer> res11 = getTasksInOrderDftReqPostOrder(tasksMap, task);
System.out.println(res11);**//note, no reverse required**
List<Integer> res12 = getTasksInOrderDftReqPreOrder(tasksMap, task);
Collections.reverse(res12);//note reverse!
System.out.println(res12);
private static List<Integer> getTasksInOrderDftReqPreOrder(Map<Integer, List<Integer>> tasksMap, int task) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
reqPreOrder(tasksMap,task,result, visited);
return result;
}
private static void reqPreOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
if(!visited.contains(task)) {
visited.add(task);
result.add(task);//pre order!
List<Integer> children = tasksMap.get(task);
if (children != null && children.size() > 0) {
for (Integer child : children) {
reqPreOrder(tasksMap,child,result, visited);
}
}
}
}
private static List<Integer> getTasksInOrderDftReqPostOrder(Map<Integer, List<Integer>> tasksMap, int task) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
reqPostOrder(tasksMap,task,result, visited);
return result;
}
private static void reqPostOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
if(!visited.contains(task)) {
visited.add(task);
List<Integer> children = tasksMap.get(task);
if (children != null && children.size() > 0) {
for (Integer child : children) {
reqPostOrder(tasksMap,child,result, visited);
}
}
result.add(task);//post order!
}
}
再帰的なpost-order-traversalでは、結果を後で反転する必要がないことに注意してください。子供が最初に印刷され、質問のあなたのタスクが最後に印刷されました。すべて順調。再帰的なpre-order-traversal(これも上に示しています)を行うことができ、結果リストの逆転が必要になります。
反復的なアプローチではそれほど単純ではありません!反復(1スタック)アプローチでは、事前順序付けトラバーサルしか実行できないため、最後に結果の配列を逆にする必要があります。
List<Integer> res1 = getTasksInOrderDftStack(tasksMap, task);
Collections.reverse(res1);//note reverse!
System.out.println(res1);
private static List<Integer> getTasksInOrderDftStack(Map<Integer, List<Integer>> tasksMap, int task) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
Stack<Integer> st = new Stack<>();
st.add(task);
visited.add(task);
while(!st.isEmpty()){
Integer node = st.pop();
List<Integer> children = tasksMap.get(node);
result.add(node);
if(children!=null && children.size() > 0){
for(Integer child:children){
if(!visited.contains(child)){
st.add(child);
visited.add(child);
}
}
}
//If you put it here - it does not matter - it is anyway a pre-order
//result.add(node);
}
return result;
}
シンプルに見えますか?
しかし、それはいくつかのインタビューの罠です。
それは次のことを意味します:再帰的なアプローチでは、深さ優先トラバーサルを実装し、必要な順序を事前または事後的に選択できます(「結果リストに追加する」の場合は、単に「印刷」の場所を変更することによって) )。反復(1スタック)アプローチを使用すると、予約注文のトラバーサルのみを簡単に行うことができるため、子を最初に印刷する必要がある場合(下のノードから上に印刷を開始する必要がある場合のほとんどすべての状況)-トラブル。問題が発生した場合は後で元に戻すことができますが、アルゴリズムに追加されます。そして、インタビュアーが彼の時計を見ている場合、それはあなたにとって問題になるかもしれません。注文後の反復走査を行うには複雑な方法がありますが、それらは存在しますが、単純ません。例:https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/
したがって、結論として、インタビューでは再帰を使用します。管理と説明が簡単です。緊急の場合でも、注文前から注文後のトラバーサルに簡単に移動できます。反復を使用すると、それほど柔軟ではありません。
私は再帰を使用して、次のように言います。
再帰のもう1つのプラス-グラフのサイクルを回避/通知する方が簡単です。
例(preudocode):
dft(n){
mark(n)
for(child: n.children){
if(marked(child))
explode - cycle found!!!
dft(child)
}
unmark(n)
}
再帰として、または練習として書くのは楽しいかもしれません。
ただし、コードを本番環境で使用する場合は、スタックオーバーフローの可能性を考慮する必要があります。
テール再帰の最適化はスタックオーバーフローを排除できますが、そうすることのトラブルを乗り越えて、環境で最適化することで信頼できることを知っておく必要があります。
n
削減ですか?データのサイズを減らすかn
、再帰するたびに半分にする場合は、一般に、スタックオーバーフローを心配する必要はありません。たとえば、プログラムがスタックオーバーフローするために深さが4,000レベルまたは10,000レベルである必要がある場合、プログラムがスタックオーバーフローするためにデータサイズは約2 4000である必要があります。この点を考慮すると、最近、最大のストレージデバイスは2 61バイトを保持できます。このようなデバイスが2 61ある場合、2 122データサイズしか処理しません。宇宙のすべての原子を見ている場合、それは2 84未満であると推定されます。宇宙の誕生から140億年前と推定されて以来、宇宙のすべてのデータとその状態をミリ秒ごとに処理する必要がある場合、それは2 153にすぎない可能性があります。したがって、プログラムが2 4000を処理できる場合データの単位またはn
、ユニバース内のすべてのデータを処理でき、プログラムはスタックオーバーフローしません。2 4000程度の大きな数値を処理する必要がない場合(4000ビットの整数)のは、通常、スタックオーバーフローを心配する必要はありません。
ただし、データのサイズや減らす場合はn
、一定量であなたは再帰たびに、あなたはときに、プログラムがうまく実行時にスタックオーバーフローに実行することができますn
です1000
が、いくつかの状況では、ときn
単になり、20000
。
したがって、スタックオーバーフローの可能性がある場合は、それを反復的な解決策にしてください。
Haskellのデータ構造を「帰納」によって設計することで、あなたの質問に答えます。これは、再帰に対する一種の「デュアル」です。そして、この二元性がどのように素晴らしいことにつながるかを示します。
単純なツリーのタイプを紹介します。
data Tree a = Branch (Tree a) (Tree a)
| Leaf a
deriving (Eq)
この定義は、「ツリーはブランチ(2つのツリーを含む)またはリーフ(データ値を含む)である」と解釈できます。したがって、葉は一種の最小限のケースです。ツリーが葉ではない場合、2つのツリーを含む複合ツリーである必要があります。これらは唯一のケースです。
木を作ってみましょう:
example :: Tree Int
example = Branch (Leaf 1)
(Branch (Leaf 2)
(Leaf 3))
ここで、ツリーの各値に1を追加するとします。これを行うには、次を呼び出します。
addOne :: Tree Int -> Tree Int
addOne (Branch a b) = Branch (addOne a) (addOne b)
addOne (Leaf a) = Leaf (a + 1)
まず、これは実際には再帰的な定義であることに注意してください。データコンストラクタのBranchとLeafをケースとして使用し(Leafは最小限であり、これらは唯一の可能なケースであるため)、関数は必ず終了します。
addOneを反復的なスタイルで書くには何が必要ですか?任意の数のブランチにループするのはどのようなものですか?
また、この種の再帰は、「ファンクター」の観点から除外されることがよくあります。以下を定義することで、ツリーをファンクタにすることができます。
instance Functor Tree where fmap f (Leaf a) = Leaf (f a)
fmap f (Branch a b) = Branch (fmap f a) (fmap f b)
そして定義:
addOne' = fmap (+1)
代数的データ型のカタモルフィズム(またはフォールド)などの他の再帰スキームを除外できます。カタモフィズムを使用して、次のように書くことができます。
addOne'' = cata go where
go (Leaf a) = Leaf (a + 1)
go (Branch a b) = Branch a b
スタックオーバーフローは、メモリ管理が組み込まれていない言語でプログラミングしている場合にのみ発生します。それ以外の場合は、関数(または関数呼び出し、STDLbsなど)に何かがあることを確認してください。再帰がなければ、GoogleやSQLなど、大きなデータ構造(クラス)やデータベースを効率的に並べ替える必要のある場所は存在しません。
再帰は、ファイルを繰り返し処理したい場合に使用する方法です。この方法で確実に確認できます* | ?grep * 'は機能します。特にパイプでのちょっとした再帰(ただし、他の人が使用できるようにする場合は、非常に多くのシステムコールを実行しないでください)。
高水準言語やclang / cppでも、バックグラウンドで同じように実装できます。