从递归到迭代的方法


349

在多年的编程中,我已经使用了很多递归来解决简单的问题,但是我完全意识到有时由于内存/速度问题而需要迭代。

因此,在很久以前的某个时候,我去尝试寻找是否存在将通用递归方法转换为迭代的任何“模式”或教科书方法,却一无所获。或至少没有什么我能记住的会有所帮助。

  • 有一般规则吗?
  • 有没有“模式”?

Answers:


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

编辑:文章Stacks and Recurmination Elimination(或文章Backup链接)进入了关于此主题的更多细节。


4
如果用Queue替换堆栈,这是否解决了反转添加顺序的问题?
SamuelWarren

2
我在纸上得出结论,这是两回事。如果您颠倒了添加它们的顺序,则可以像往常一样前进,但是遍历仍然是深度优先搜索。但是,如果您现在将整个流程更改为队列,那么您将进行广度优先而不是深度优先遍历。
皮特

1
我最近以一种通用的方式进行了此操作,方法是将我的节点访问功能替换(node)->()(node)->[actions]action是() -> [actions]。然后在外面,您只需从堆栈中弹出一个动作/继续动作,应用/执行它,然后按顺序反向执行它在堆栈上返回的动作,然后重复执行即可。然/复杂的遍历,你只要抓住你会在引用计数指针已经局部堆栈变量您关闭了在你的thunk,那么后续的thunk可以在前面的子遍历等的结果队伍
experquisite

6
有时我们为了避免stackoverflow而避免递归。但是维护我们自己的堆栈也会导致堆栈溢出。那么,为什么我们要使用自己的堆栈来实现递归呢?
朱丽

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

显然,此示例不检查堆栈边界...实际上,您可以根据给定的left和right值的最坏情况确定堆栈的大小。但是你明白了。


1
关于如何计算最大堆栈以分配给特定递归的任何想法?
lexicalscope 2012年

@lexicalscope假设您在中有一个递归算法O(N) = O(R*L),其中L是“针对第r层”的复杂度之和,例如,在这种情况下,您需要O(N)进行分区的每一步工作,递归深度为O(R),即最坏情况O(N)O(logN)这里是平均情况。
卡雷斯(Caleth)'17

48

似乎没有人解决过递归函数在主体中多次调用自身的问题,并处理了返回到递归中的特定点(即不是原始递归)的问题。据说每次递归都可以转换成迭代,因此看来这应该是可能的。

我只是想出一个如何执行此操作的C#示例。假设您具有以下递归函数,其作用类似于后序遍历,并且AbcTreeNode是具有指针a,b,c的三叉树。

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 '02

1
这必须是我所见过的最好的示例,它可以在方法中进行多个递归调用的情况下模拟调用堆栈递归。不错的工作。
CCS 2014年

1
您在“我似乎没有人解决过递归函数在主体中多次调用自身并处理返回到递归中的特定点的问题”一文中提到了我,然后我已经投票了。好的,现在我将阅读其余的答案,看看我的过早投票是否合理。(因为我非常需要知道答案)。
mydoghasworms

1
@mydoghasworms-过了很久才回到这个问题,甚至花了一点时间来记住我的想法。希望答案有所帮助。
T. Webster

1
我喜欢这种解决方案的想法,但对我来说似乎很困惑。我在python中为二进制树写了简化版,也许它可以帮助某人理解这个想法:gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac
azurkin

33

努力进行递归调用Tail Recursion(递归,其中最后一条语句是递归调用)。一旦有了这些,将其转换为迭代通常非常容易。



大量的口译员(即最著名的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
我同意你的第一点,但我认为我误会了第二段。考虑通过仅复制内存copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];空间来克隆数组,并且时间复杂度都是基于数据大小的O(N),但这显然是一种迭代算法。
Ponkadoodle

13

堆和递归消除文章捕获外在上堆栈帧的想法,但不提供直接的和可重复的方式转换。下面是一个。

在转换为迭代代码时,必须意识到递归调用可能发生在任意深度的代码块中。它不仅是参数,而且是返回仍然要执行的逻辑的点以及重要的,参与后续条件的变量的状态。下面是一种最少更改即可转换为迭代代码的非常简单的方法。

考虑以下递归代码:

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。
JanX2

@ JanX2,stackitem不得在未初始化的情况下推送。但是可以,初始化为0会捕获错误。
Chethan 2014年

为什么两个返回地址都没有设置为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执行复杂功能的对象的列表。或者,您可以选择相反的方向,并使其成为更简单类型的列表(例如,“任务”可能是的堆栈中包含4个元素int,而不是的堆栈中包含一个元素Task)。

这意味着堆栈的内存在堆中,而不是在Java执行堆栈中,但这对您有更多的控制权可能很有用。


3

要寻找的一种模式是在函数末尾进行递归调用(所谓的尾递归)。可以轻松地将其替换一会儿。例如,函数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;
     }
}

这样就消除了第二个递归调用。


3
依旧看起来对我来说是递归的... :)

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

通常,并非总是可以避免为多次调用自身(甚至一次)的递归函数编写堆栈。但是,对于这种特定结构,这是可能的。想法是将所有节点放到一个列表中。这是通过将当前节点放在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)。当我们进行递归调用时,没有新的事情发生。一个函数通过传递相同的类型和相似的名称变量来调用自身(但很显然,存储在变量中的值是不同的,只是名称保持不变)。但是,在每次调用之前,函数都会保存其状态,并且保存过程将继续。保存在堆栈上。

现在,堆栈开始播放了。

因此,如果您编写一个迭代程序并每次将状态保存在堆栈上,然后在需要时从堆栈中弹出值,那么您已成功将递归程序转换为迭代程序!

证明是简单和分析的。

在递归中,计算机维护堆栈,而在迭代版本中,您将必须手动维护堆栈。

想一想,只需将深度优先搜索(在图形上)递归程序转换为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;
}

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)的意思是-将项目添加到堆栈中,意思是“您需要在第2行继续使用局部变量状态node = A”-在第0行使用node = B。

要执行代码,系统将按照说明进行操作。遇到函数调用时,系统将需要的信息推送回原来的位置,运行函数代码,并在函数完成时弹出有关继续执行的信息。


0

链接提供了一些解释,并提出了保持“位置”的思想,以便能够到达几个递归调用之间的确切位置:

但是,所有这些示例都描述了进行固定次数递归调用的方案。当您遇到类似以下情况时,事情会变得更加棘手:

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

0

通过使用将多个迭代器提供程序连接在一起的惰性迭代器(返回迭代器的lambda表达式),存在一种将递归遍历转换为迭代器的一般方法。请参阅我的将递归遍历转换为迭代器


0

我的示例在Clojure中,但是应该很容易翻译成任何语言。

给定此函数,StackOverflow对于n的较大值:

(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)))))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.