私は長年のプログラミングでかなりの再帰を使用して単純な問題を解決してきましたが、メモリ/速度の問題のために反復が必要な場合があることは十分に承知しています。
それで、私は非常に過去のある時期に、一般的な再帰アプローチを反復に変換する「パターン」またはテキストブックの方法が存在するかどうかを調べてみましたが、何も見つかりませんでした。または、少なくとも私がそれを覚えていることは何の助けにもなりません。
- 一般的なルールはありますか?
- 「パターン」はありますか?
私は長年のプログラミングでかなりの再帰を使用して単純な問題を解決してきましたが、メモリ/速度の問題のために反復が必要な場合があることは十分に承知しています。
それで、私は非常に過去のある時期に、一般的な再帰アプローチを反復に変換する「パターン」またはテキストブックの方法が存在するかどうかを調べてみましたが、何も見つかりませんでした。または、少なくとも私がそれを覚えていることは何の助けにもなりません。
回答:
通常、私は通常再帰関数に渡されるパラメーターをスタックにプッシュすることにより、再帰アルゴリズムを反復アルゴリズムに置き換えます。実際、プログラムスタックを独自のスタックに置き換えています。
Stack<Object> stack;
stack.push(first_object);
while( !stack.isEmpty() ) {
// Do something
my_object = stack.pop();
// Push other objects on the stack.
}
注:内部に複数の再帰呼び出しがあり、呼び出しの順序を保持したい場合は、逆の順序でスタックに追加する必要があります。
foo(first);
foo(second);
に置き換える必要があります
stack.push(second);
stack.push(first);
編集:記事のスタックと再帰の削除(または記事のバックアップリンク)は、この主題の詳細を説明しています。
(node)->()
を(node)->[actions]
アクションのある場所に置き換えることです() -> [actions]
。次に、外側で、スタックからアクション/継続をポップし、適用/実行し、スタックに返されたアクションを逆の順序でプッシュして繰り返します。偶発/複雑な横断、あなたはちょうどあなたが近いの上にサンクで、その後のサンクは、前のサブトラバーサルなどの結果次第でできることを参照カウントポインタ内のローカルスタック変数であったであろうものをキャプチャ
new
と、スタックではなくヒープ上にオブジェクトを作成できます。スタックとは異なり、ヒープにはメモリ制限がありません。gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html
実際、最も一般的な方法は、独自のスタックを保持することです。これがCの再帰的なクイックソート関数です:
void quicksort(int* array, int left, int right)
{
if(left >= right)
return;
int index = partition(array, left, right);
quicksort(array, left, index - 1);
quicksort(array, index + 1, right);
}
以下は、独自のスタックを保持することにより、それを反復的にする方法です。
void quicksort(int *array, int left, int right)
{
int stack[1024];
int i=0;
stack[i++] = left;
stack[i++] = right;
while (i > 0)
{
right = stack[--i];
left = stack[--i];
if (left >= right)
continue;
int index = partition(array, left, right);
stack[i++] = left;
stack[i++] = index - 1;
stack[i++] = index + 1;
stack[i++] = right;
}
}
明らかに、この例はスタック境界をチェックしません...そして実際には、左と右の値が与えられた最悪のケースに基づいてスタックのサイズを決めることができます。しかし、あなたはアイデアを理解します。
O(N) = O(R*L)
。ここで、L
は「層rの」複雑度の合計です。たとえば、この場合O(N)
、パーティション化を実行する各ステップで作業し、再帰の深さはO(R)
、つまりO(N)
、O(logN)
ここでは最悪の場合の平均ケースです。
再帰関数が本体内で自分自身を2回以上呼び出し、再帰の特定のポイントへの戻りを処理する(つまり、プリミティブ-再帰的でない)場合、誰も対処していないようです。すべての再帰を反復に変えることができると言われているため、これは可能であるはずです。
これを行う方法のC#の例を思いついたところです。次の再帰関数があり、これはポストオーダートラバーサルのように機能し、AbcTreeNodeはポインターa、b、cを持つ3項ツリーであるとします。
public static void AbcRecursiveTraversal(this AbcTreeNode x, List<int> list) {
if (x != null) {
AbcRecursiveTraversal(x.a, list);
AbcRecursiveTraversal(x.b, list);
AbcRecursiveTraversal(x.c, list);
list.Add(x.key);//finally visit root
}
}
反復的なソリューション:
int? address = null;
AbcTreeNode x = null;
x = root;
address = A;
stack.Push(x);
stack.Push(null)
while (stack.Count > 0) {
bool @return = x == null;
if (@return == false) {
switch (address) {
case A://
stack.Push(x);
stack.Push(B);
x = x.a;
address = A;
break;
case B:
stack.Push(x);
stack.Push(C);
x = x.b;
address = A;
break;
case C:
stack.Push(x);
stack.Push(null);
x = x.c;
address = A;
break;
case null:
list_iterative.Add(x.key);
@return = true;
break;
}
}
if (@return == true) {
address = (int?)stack.Pop();
x = (AbcTreeNode)stack.Pop();
}
}
再帰的な呼び出しであるTail Recursion(最後のステートメントが再帰的な呼び出しである再帰)を作成するように努めます。それができたら、それを反復に変換することは一般的に非常に簡単です。
まあ、一般的に、再帰は単にストレージ変数を使用するだけで反復として模倣できます。再帰と反復は一般に同等です。ほとんどの場合、一方を他方に変換できます。末尾再帰関数は非常に簡単に反復関数に変換されます。アキュムレータ変数をローカル変数にして、再帰ではなく反復するだけです。C ++での例を次に示します(Cはデフォルトの引数を使用するためのものではありませんでした)。
// tail-recursive
int factorial (int n, int acc = 1)
{
if (n == 1)
return acc;
else
return factorial(n - 1, acc * n);
}
// iterative
int factorial (int n)
{
int acc = 1;
for (; n > 1; --n)
acc *= n;
return acc;
}
私のことを知っているので、おそらくコードを間違えたかもしれませんが、アイデアはそこにあります。
スタックを使用しても、再帰アルゴリズムは反復に変換されません。通常の再帰は関数ベースの再帰であり、スタックを使用すると、スタックベースの再帰になります。しかし、それでも再帰です。
再帰的アルゴリズムの場合、空間の複雑度はO(N)で、時間の複雑度はO(N)です。反復アルゴリズムの場合、空間の複雑度はO(1)で、時間の複雑度はO(N)です。
しかし、スタックを使用する場合、複雑さの点では同じままです。反復に変換できるのは末尾再帰だけだと思います。
copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];
スペースをコピーするだけで配列のクローンを作成することを検討してください。時間の複雑さはどちらもデータのサイズに基づいてO(N)ですが、これは明らかに反復アルゴリズムです。
スタックと再帰の削除記事は、ヒープ上のスタックフレームを外部のアイデアをキャプチャしますが、提供していない、簡単で再現変換する方法を。以下はその1つです。
反復コードに変換するとき、再帰呼び出しが任意の深いコードブロックから発生する可能性があることに注意する必要があります。そのパラメータだけでなく、実行する必要のあるロジックに戻るポイントや、重要な後続の条件に参加する変数の状態も重要です。以下は、最小限の変更で反復コードに変換する非常に簡単な方法です。
この再帰的なコードを考えてみましょう:
struct tnode
{
tnode(int n) : data(n), left(0), right(0) {}
tnode *left, *right;
int data;
};
void insertnode_recur(tnode *node, int num)
{
if(node->data <= num)
{
if(node->right == NULL)
node->right = new tnode(num);
else
insertnode(node->right, num);
}
else
{
if(node->left == NULL)
node->left = new tnode(num);
else
insertnode(node->left, num);
}
}
反復コード:
// Identify the stack variables that need to be preserved across stack
// invocations, that is, across iterations and wrap them in an object
struct stackitem
{
stackitem(tnode *t, int n) : node(t), num(n), ra(0) {}
tnode *node; int num;
int ra; //to point of return
};
void insertnode_iter(tnode *node, int num)
{
vector<stackitem> v;
//pushing a stackitem is equivalent to making a recursive call.
v.push_back(stackitem(node, num));
while(v.size())
{
// taking a modifiable reference to the stack item makes prepending
// 'si.' to auto variables in recursive logic suffice
// e.g., instead of num, replace with si.num.
stackitem &si = v.back();
switch(si.ra)
{
// this jump simulates resuming execution after return from recursive
// call
case 1: goto ra1;
case 2: goto ra2;
default: break;
}
if(si.node->data <= si.num)
{
if(si.node->right == NULL)
si.node->right = new tnode(si.num);
else
{
// replace a recursive call with below statements
// (a) save return point,
// (b) push stack item with new stackitem,
// (c) continue statement to make loop pick up and start
// processing new stack item,
// (d) a return point label
// (e) optional semi-colon, if resume point is an end
// of a block.
si.ra=1;
v.push_back(stackitem(si.node->right, si.num));
continue;
ra1: ;
}
}
else
{
if(si.node->left == NULL)
si.node->left = new tnode(si.num);
else
{
si.ra=2;
v.push_back(stackitem(si.node->left, si.num));
continue;
ra2: ;
}
}
v.pop_back();
}
}
コードの構造が依然として再帰ロジックに忠実であり、変更が最小限であるため、バグの数が少なくなっていることに注意してください。比較のため、++と-で変更をマークしました。v.push_back以外の新しく挿入されたブロックのほとんどは、変換された反復ロジックに共通です。
void insertnode_iter(tnode *node, int num)
{
+++++++++++++++++++++++++
vector<stackitem> v;
v.push_back(stackitem(node, num));
while(v.size())
{
stackitem &si = v.back();
switch(si.ra)
{
case 1: goto ra1;
case 2: goto ra2;
default: break;
}
------------------------
if(si.node->data <= si.num)
{
if(si.node->right == NULL)
si.node->right = new tnode(si.num);
else
{
+++++++++++++++++++++++++
si.ra=1;
v.push_back(stackitem(si.node->right, si.num));
continue;
ra1: ;
-------------------------
}
}
else
{
if(si.node->left == NULL)
si.node->left = new tnode(si.num);
else
{
+++++++++++++++++++++++++
si.ra=2;
v.push_back(stackitem(si.node->left, si.num));
continue;
ra2: ;
-------------------------
}
}
+++++++++++++++++++++++++
v.pop_back();
}
-------------------------
}
stackitem
ます。オブジェクトにのガベージ値が割り当てられますra
。すべてが最も類似したケースでも機能しra
ますが、偶然にも1または2になると、正しくない動作が発生します。解決策は、初期化することであるra
0に
stackitem
初期化せずにプッシュしてはなりません。しかし、はい、0に初期化するとエラーが発生します。
v.pop_back()
代わりに両方のアドレスがステートメントに設定されていないのはなぜですか?
時間をつぶすだけ...再帰関数
void foo(Node* node)
{
if(node == NULL)
return;
// Do something with node...
foo(node->left);
foo(node->right);
}
に変換することができます
void foo(Node* node)
{
if(node == NULL)
return;
// Do something with node...
stack.push(node->right);
stack.push(node->left);
while(!stack.empty()) {
node1 = stack.pop();
if(node1 == NULL)
continue;
// Do something with node1...
stack.push(node1->right);
stack.push(node1->left);
}
}
一般に、スタックオーバーフローを回避する手法は再帰関数用であり、トランポリン手法と呼ばれ、Java開発者によって広く採用されています。
しかし、C#のために少しのヘルパーメソッドがあり、ここで変更ロジックに必要かで、理解しやすいコードを作成せずに、反復にあなたの再帰関数をオンにします。C#はすばらしい言語であり、驚くべきことが可能です。
メソッドの一部をヘルパーメソッドでラップすることで機能します。たとえば、次の再帰関数:
int Sum(int index, int[] array)
{
//This is the termination condition
if (int >= array.Length)
//This is the returning value when termination condition is true
return 0;
//This is the recursive call
var sumofrest = Sum(index+1, array);
//This is the work to do with the current item and the
//result of recursive call
return array[index]+sumofrest;
}
になる:
int Sum(int[] ar)
{
return RecursionHelper<int>.CreateSingular(i => i >= ar.Length, i => 0)
.RecursiveCall((i, rv) => i + 1)
.Do((i, rv) => ar[i] + rv)
.Execute(0);
}
実際にスタックが必要なものについて考える:
再帰のパターンを次のように考えると、
if(task can be done directly) {
return result of doing task directly
} else {
split task into two or more parts
solve for each part (possibly by recursing)
return result constructed by combining these solutions
}
たとえば、古典的なハノイの塔
if(the number of discs to move is 1) {
just move it
} else {
move n-1 discs to the spare peg
move the remaining disc to the target peg
move n-1 discs from the spare peg to the target peg, using the current peg as a spare
}
これは、次のように再表現することにより、明示的なスタックで動作するループに変換できます。
place seed task on stack
while stack is not empty
take a task off the stack
if(task can be done directly) {
Do it
} else {
Split task into two or more parts
Place task to consolidate results on stack
Place each task on stack
}
}
ハノイの塔の場合、これは次のようになります。
stack.push(new Task(size, from, to, spare));
while(! stack.isEmpty()) {
task = stack.pop();
if(task.size() = 1) {
just move it
} else {
stack.push(new Task(task.size() -1, task.spare(), task,to(), task,from()));
stack.push(new Task(1, task.from(), task.to(), task.spare()));
stack.push(new Task(task.size() -1, task.from(), task.spare(), task.to()));
}
}
ここでは、スタックの定義方法に関してかなりの柔軟性があります。スタックを、Command
洗練された処理を行うオブジェクトのリストにすることができます。または、反対の方向に進んで、より単純なタイプのリストにすることもできます(たとえば、「タスク」は、スタックのint
1つの要素ではなく、スタックの4 つの要素になる場合がありますTask
)。
これはすべて、スタックのメモリがJava実行スタックではなくヒープ内にあることを意味しますが、これはそれをより詳細に制御できるので便利です。
探すべきパターンの1つは、関数の最後での再帰呼び出し(いわゆる末尾再帰)です。これはしばらくすると簡単に交換できます。たとえば、関数foo:
void foo(Node* node)
{
if(node == NULL)
return;
// Do something with node...
foo(node->left);
foo(node->right);
}
fooの呼び出しで終了します。これは次のように置き換えることができます:
void foo(Node* node)
{
while(node != NULL)
{
// Do something with node...
foo(node->left);
node = node->right;
}
}
これにより、2番目の再帰呼び出しが排除されます。
この質問の複製としてクローズされた質問には、非常に具体的なデータ構造がありました。
ノードの構造は次のとおりです。
typedef struct {
int32_t type;
int32_t valueint;
double valuedouble;
struct cNODE *next;
struct cNODE *prev;
struct cNODE *child;
} cNODE;
再帰的削除関数は次のようになります。
void cNODE_Delete(cNODE *c) {
cNODE*next;
while (c) {
next=c->next;
if (c->child) {
cNODE_Delete(c->child)
}
free(c);
c=next;
}
}
一般に、自分自身を複数回(または1回でも)呼び出す再帰関数のスタックを回避することが常に可能であるとは限りません。ただし、この特定の構造では、それが可能です。アイデアは、すべてのノードを単一のリストにフラット化することです。これは、現在のノードをchild
最上行のリストの最後に配置することで実現されます。
void cNODE_Delete (cNODE *c) {
cNODE *tmp, *last = c;
while (c) {
while (last->next) {
last = last->next; /* find last */
}
if ((tmp = c->child)) {
c->child = NULL; /* append child to last */
last->next = tmp;
tmp->prev = last;
}
tmp = c->next; /* remove current */
free(c);
c = tmp;
}
}
この手法は、確定的なトポロジー順序を使用してDAGに削減できる任意のデータリンク構造に適用できます。現在のノードの子は、最後の子が他のすべての子を採用するように再配置されます。次に、現在のノードを削除して、トラバーサルを残りの子に反復できます。
再帰は、ある関数を他の関数から呼び出すプロセスにすぎません。このプロセスは、関数をそれ自体で呼び出すことによってのみ実行されます。ある関数が別の関数を呼び出すと、最初の関数がその状態(変数)を保存し、呼び出された関数に制御を渡します。呼び出された関数は、fun1(a)がfun2(a)を呼び出すことができる変数と同じ名前を使用して呼び出すことができます。再帰呼び出しを行っても、新しいことは何も起こりません。1つの関数は、同じ型および類似の名前変数を渡すことによってそれ自体を呼び出します(ただし、変数に格納されている値は異なり、名前だけが同じままです)。しかし、すべての呼び出しの前に、関数はその状態を保存し、この保存プロセスは続行されます。貯蓄はスタックで行われます。
今、スタックはプレイに入ります。
したがって、反復プログラムを作成してその都度状態をスタックに保存し、必要に応じてスタックから値をポップすると、再帰プログラムが反復プログラムに正常に変換されます。
証明は単純で分析的です。
再帰ではコンピューターがスタックを維持し、反復バージョンでは手動でスタックを維持する必要があります。
考え直してください。(グラフ上の)深さ優先検索再帰プログラムをdfs反復プログラムに変換するだけです。
ではごきげんよう!
スタックを使用して再帰関数を反復関数に変換する、もう1つの単純で完全な例。
#include <iostream>
#include <stack>
using namespace std;
int GCD(int a, int b) { return b == 0 ? a : GCD(b, a % b); }
struct Par
{
int a, b;
Par() : Par(0, 0) {}
Par(int _a, int _b) : a(_a), b(_b) {}
};
int GCDIter(int a, int b)
{
stack<Par> rcstack;
if (b == 0)
return a;
rcstack.push(Par(b, a % b));
Par p;
while (!rcstack.empty())
{
p = rcstack.top();
rcstack.pop();
if (p.b == 0)
continue;
rcstack.push(Par(p.b, p.a % p.b));
}
return p.a;
}
int main()
{
//cout << GCD(24, 36) << endl;
cout << GCDIter(81, 36) << endl;
cin.get();
return 0;
}
システムが再帰関数を取得し、スタックを使用して実行する方法の大まかな説明:
これは、詳細なしでアイデアを示すことを目的としています。グラフのノードを出力する次の関数について考えてみます。
function show(node)
0. if isleaf(node):
1. print node.name
2. else:
3. show(node.left)
4. show(node)
5. show(node.right)
グラフの例:A-> B A-> C show(A)はB、A、Cを出力します
関数呼び出しは、ローカル状態と継続ポイントを保存して、戻って呼び出したい関数にジャンプできるようにすることを意味します。
たとえば、show(A)が実行を開始するとします。3行目の関数呼び出し。show(B)は、「ローカル変数の状態node = Aで行2に進む必要がある」という意味のスタックに項目を追加する-node = Bで行0に移動する。
コードを実行するために、システムは命令を実行します。関数呼び出しが検出されると、システムは必要な情報を元の場所に戻し、関数コードを実行します。関数が完了すると、続行する必要がある場所に関する情報をポップします。
複数の反復子サプライヤを連結する遅延反復子(反復子を返すラムダ式)を使用して、再帰的トラバーサルを反復子に変換する一般的な方法があります。再帰トラバーサルからイテレーターへの変換を参照してください。
私の例はClojureにありますが、どの言語にもかなり簡単に翻訳できます。
StackOverflow
nの大きな値に対してs となる次の関数があるとします。
(defn factorial [n]
(if (< n 2)
1
(*' n (factorial (dec n)))))
独自のスタックを使用するバージョンを次のように定義できます。
(defn factorial [n]
(loop [n n
stack []]
(if (< n 2)
(return 1 stack)
;; else loop with new values
(recur (dec n)
;; push function onto stack
(cons (fn [n-1!]
(*' n n-1!))
stack)))))
場所return
:のように定義されます
(defn return
[v stack]
(reduce (fn [acc f]
(f acc))
v
stack))
これは、たとえばackermann関数など、より複雑な関数でも機能します。
(defn ackermann [m n]
(cond
(zero? m)
(inc n)
(zero? n)
(recur (dec m) 1)
:else
(recur (dec m)
(ackermann m (dec n)))))
に変換することができます:
(defn ackermann [m n]
(loop [m m
n n
stack []]
(cond
(zero? m)
(return (inc n) stack)
(zero? n)
(recur (dec m) 1 stack)
:else
(recur m
(dec n)
(cons #(ackermann (dec m) %)
stack)))))