再帰から反復への道


349

私は長年のプログラミングでかなりの再帰を使用して単純な問題を解決してきましたが、メモリ/速度の問題のために反復が必要な場合があることは十分に承知しています。

それで、私は非常に過去のある時期に、一般的な再帰アプローチを反復に変換する「パターン」またはテキストブックの方法が存在するかどうかを調べてみましたが、何も見つかりませんでした。または、少なくとも私がそれを覚えていることは何の助けにもなりません。

  • 一般的なルールはありますか?
  • 「パターン」はありますか?

4
私はこのシリーズが有益であることを発見しました:blog.moertel.com/posts/2013-05-11-recursive-to-iterative.html
オリオンラッシュ2018

回答:


333

通常、私は通常再帰関数に渡されるパラメーターをスタックにプッシュすることにより、再帰アルゴリズムを反復アルゴリズムに置き換えます。実際、プログラムスタックを独自のスタックに置き換えています。

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);

編集:記事のスタックと再帰の削除(または記事のバックアップリンク)は、この主題の詳細を説明しています。


4
スタックをキューに置き換えると、追加順序を逆にする問題が解決しませんか?
SamuelWarren 2013年

2
私は紙の上でそれを考え出した、そしてそれらは二つの異なるものである。追加した順序を逆にすると、通常どおり順方向にトラバースしますが、トラバースは依然として深さ優先検索です。しかし、全体をキューに変更すると、深さ優先のトラバースではなく幅優先のトラバーサルが実行されます。
ピート2013年

1
私は最近、一般的な方法でこれを行いました。ノードの訪問関数(node)->()(node)->[actions]アクションのある場所に置き換えることです() -> [actions]。次に、外側で、スタックからアクション/継続をポップし、適用/実行し、スタックに返されたアクションを逆の順序でプッシュして繰り返します。偶発/複雑な横断、あなたはちょうどあなたが近いの上にサンクで、その後のサンクは、前のサブトラバーサルなどの結果次第でできることを参照カウントポインタ内のローカルスタック変数であったであろうものをキャプチャ
experquisite

6
場合によっては、スタックオーバーフローを回避するために再帰を回避します。ただし、独自のスタックを維持すると、スタックオーバーフローが発生します。では、なぜ独自のスタックで再帰を実装する必要があるのでしょうか。
朱里

8
@ZhuLi使用するnewと、スタックではなくヒープ上にオブジェクトを作成できます。スタックとは異なり、ヒープにはメモリ制限がありません。gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html
yuqliを

77

実際、最も一般的な方法は、独自のスタックを保持することです。これが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;
    }
}

明らかに、この例はスタック境界をチェックしません...そして実際には、左と右の値が与えられた最悪のケースに基づいてスタックのサイズを決めることができます。しかし、あなたはアイデアを理解します。


1
特定の再帰に割り当てる最大スタックを計算する方法に関するアイデアはありますか?
レキシカルスコープ2012年

@lexicalscopeに再帰アルゴリズムがあるとしますO(N) = O(R*L)。ここで、Lは「層rの」複雑度の合計です。たとえば、この場合O(N)、パーティション化を実行する各ステップで作業し、再帰の深さはO(R)、つまりO(N)O(logN)ここでは最悪の場合の平均ケースです。
Caleth、2017

48

再帰関数が本体内で自分自身を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();
            }


        }

5
それは本当に便利です、あなたの投稿のおかげでそれ自体をn回起動する反復の反復バージョンを書かなければなりませんでした。
Wojciech Kulik 2012

1
これは、メソッド内で複数の再帰呼び出しが行われている状況で、コールスタックの再帰をエミュレートするためにこれまでに見た中で最高の例でなければなりません。良くやった。
CCS

1
「再帰関数が本体で2回以上自分自身を呼び出し、再帰の特定のポイントへの戻りを処理する場所には誰も対処していないようです」と私に言ったので、すでに賛成しています。さて、これからあなたの答えの残りを読み、私の時期尚早の賛成が正当化されたかどうかを確認します。(私は必死にその答えを知る必要があるので)。
mydoghasworms 2015年

1
@mydoghasworms-久しぶりにこの質問に戻ると、私が何を考えていたかを思い出すのに少し時間がかかりました。答えが役に立てば幸いです。
T.ウェブスター

1
私はこの解決策のアイデアが好きでしたが、それは私を混乱させるようでした。私はPythonでバイナリツリーの簡略版を書きました。多分それは誰かがアイデアを理解するのに役立つかもしれません:gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac
azurkin

33

再帰的な呼び出しであるTail Recursion(最後のステートメントが再帰的な呼び出しである再帰)を作成するように努めます。それができたら、それを反復に変換することは一般的に非常に簡単です。


2
一部のJITの変換テール再帰:ibm.com/developerworks/java/library/j-diag8.html
Liran Orevi 2009年

多くのインタープリター(つまり、Schemeが最もよく知られている)は、末尾再帰を最適化します。特定の最適化により、GCCが末尾再帰を実行することを知っています(Cはそのような最適化の奇妙な選択ですが)。
new123456 2011

19

まあ、一般的に、再帰は単にストレージ変数を使用するだけで反復として模倣できます。再帰と反復は一般に同等です。ほとんどの場合、一方を他方に変換できます。末尾再帰関数は非常に簡単に反復関数に変換されます。アキュムレータ変数をローカル変数にして、再帰ではなく反復するだけです。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;
}

私のことを知っているので、おそらくコードを間違えたかもしれませんが、アイデアはそこにあります。


14

スタックを使用しても、再帰アルゴリズムは反復に変換されません。通常の再帰は関数ベースの再帰であり、スタックを使用すると、スタックベースの再帰になります。しかし、それでも再帰です。

再帰的アルゴリズムの場合、空間の複雑度はO(N)で、時間の複雑度はO(N)です。反復アルゴリズムの場合、空間の複雑度はO(1)で、時間の複雑度はO(N)です。

しかし、スタックを使用する場合、複雑さの点では同じままです。反復に変換できるのは末尾再帰だけだと思います。


1
私はあなたの最初の意見に同意しますが、私は2番目の段落を誤解していると思います。メモリcopy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];スペースをコピーするだけで配列のクローンを作成することを検討してください。時間の複雑さはどちらもデータのサイズに基づいてO(N)ですが、これは明らかに反復アルゴリズムです。
Ponkadoodle 2013

13

スタックと再帰の削除記事は、ヒープ上のスタックフレームを外部のアイデアをキャプチャしますが、提供していない、簡単で再現変換する方法を。以下はその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になると、正しくない動作が発生します。解決策は、初期化することであるra0に
JanX2

@ JanX2、stackitem初期化せずにプッシュしてはなりません。しかし、はい、0に初期化するとエラーが発生します。
Chethan

v.pop_back()代わりに両方のアドレスがステートメントに設定されていないのはなぜですか?
is7s、2015

7

Googleで「継続合格スタイル」を検索します。末尾再帰スタイルに変換する一般的な手順があります。末尾再帰関数をループに変換する一般的な手順もあります。


6

時間をつぶすだけ...再帰関数

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);
    }

}

上記の例は、バイナリ検索ツリーでの反復dfsの再帰の例です:)
Amit

5

一般に、スタックオーバーフローを回避する手法は再帰関数用であり、トランポリン手法と呼ばれ、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);
}

4

実際にスタックが必要なものについて考える:

再帰のパターンを次のように考えると、

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洗練された処理を行うオブジェクトのリストにすることができます。または、反対の方向に進んで、より単純なタイプのリストにすることもできます(たとえば、「タスク」は、スタックのint1つの要素ではなく、スタックの4 つの要素になる場合がありますTask)。

これはすべて、スタックのメモリがJava実行スタックではなくヒープ内にあることを意味しますが、これはそれをより詳細に制御できるので便利です。


3

探すべきパターンの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番目の再帰呼び出しが排除されます。


3
それでも私には再帰的に見えます... :)
nathan

2
ええ、ええ-しかし、それは再帰的なものの半分です。他の再帰を取り除くには、別の手法を使用する必要があります...
Mark Bessey

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に削減できる任意のデータリンク構造に適用できます。現在のノードの子は、最後の子が他のすべての子を採用するように再配置されます。次に、現在のノードを削除して、トラバーサルを残りの子に反復できます。


1

再帰は、ある関数を他の関数から呼び出すプロセスにすぎません。このプロセスは、関数をそれ自体で呼び出すことによってのみ実行されます。ある関数が別の関数を呼び出すと、最初の関数がその状態(変数)を保存し、呼び出された関数に制御を渡します。呼び出された関数は、fun1(a)がfun2(a)を呼び出すことができる変数と同じ名前を使用して呼び出すことができます。再帰呼び出しを行っても、新しいことは何も起こりません。1つの関数は、同じ型および類似の名前変数を渡すことによってそれ自体を呼び出します(ただし、変数に格納されている値は異なり、名前だけが同じままです)。しかし、すべての呼び出しの前に、関数はその状態を保存し、この保存プロセスは続行されます。貯蓄はスタックで行われます。

今、スタックはプレイに入ります。

したがって、反復プログラムを作成してその都度状態をスタックに保存し、必要に応じてスタックから値をポップすると、再帰プログラムが反復プログラムに正常に変換されます。

証明は単純で分析的です。

再帰ではコンピューターがスタックを維持し、反復バージョンでは手動でスタックを維持する必要があります。

考え直してください。(グラフ上の)深さ優先検索再帰プログラムをdfs反復プログラムに変換するだけです。

ではごきげんよう!


1

スタックを使用して再帰関数を反復関数に変換する、もう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;
}

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に移動する。

コードを実行するために、システムは命令を実行します。関数呼び出しが検出されると、システムは必要な情報を元の場所に戻し、関数コードを実行します。関数が完了すると、続行する必要がある場所に関する情報をポップします。


0

このリンクは、いくつかの説明を提供し、いくつかの再帰呼び出しの間の正確な場所に到達できるように「場所」を維持するアイデアを提案します。

ただし、これらの例はすべて、再帰呼び出しが一定回数行われるシナリオを示しています。あなたが次のようなものを持っている場合、物事はよりトリッキーになります:

function rec(...) {
  for/while loop {
    var x = rec(...)
    // make a side effect involving return value x
  }
}


0

私の例はClojureにありますが、どの言語にもかなり簡単に翻訳できます。

StackOverflownの大きな値に対して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)))))
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.