从递归到迭代的方法

Posted

技术标签:

【中文标题】从递归到迭代的方法【英文标题】:Way to go from recursion to iteration 【发布时间】:2010-09-14 15:31:57 【问题描述】:

在我多年的编程工作中,我经常使用递归来解决简单的问题,但我完全清楚,有时由于内存/速度问题,您需要迭代。

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

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

【问题讨论】:

我发现这个系列内容丰富:blog.moertel.com/posts/2013-05-11-recursive-to-iterative.html 【参考方案1】:

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

【讨论】:

一些 JIT 的变换尾递归:ibm.com/developerworks/java/library/j-diag8.html 大量的解释器(即最著名的Scheme)将很好地优化尾递归。我知道经过一定优化的 GCC 会进行尾递归(尽管 C 是这种优化的奇怪选择)。【参考方案2】:

在 Google 上搜索“继续传递风格”。有一个转换为尾递归样式的通用过程;还有一个将尾递归函数变成循环的通用过程。

【讨论】:

【参考方案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;
     

这消除了第二次递归调用。

【讨论】:

在我看来仍然是递归的...... :) 嗯,是的 - 但它是递归的一半。摆脱其他递归将需要使用另一种技术......【参考方案4】:

嗯,一般来说,递归可以通过简单地使用存储变量来模拟为迭代。请注意,递归和迭代通常是等价的;一个几乎总是可以转换为另一个。尾递归函数很容易转换为迭代函数。只需将累加器变量设为本地变量,然后迭代而不是递归。这是 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;

了解我,我可能在代码中犯了一个错误,但想法就在那里。

【讨论】:

【参考方案5】:

确实,最常见的方法是保留自己的堆栈。这是 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;
    

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

【讨论】:

关于如何计算出为特定递归分配的最大堆栈的任何想法? @lexicalscope 假设您在O(N) = O(R*L) 中有一个递归算法,其中L 是“r 层”复杂度的总和,例如在这种情况下,您在每个步骤都有O(N) 工作进行分区,递归深度为O(R),即最坏情况O(N),平均情况O(logN) @lexicalscope 总是将最长部分的边界推入堆栈并继续循环以处理分区的最短部分。这样,堆栈保证在数组大小中是对数的。【参考方案6】:

通常,我通过将通常传递给递归函数的参数推入堆栈来用迭代算法替换递归算法。事实上,您正在用您自己的堆栈替换程序堆栈。

var stack = [];
stack.push(firstObject);

// while not empty
while (stack.length) 

    // Pop off end of stack.
    obj = stack.pop();

    // Do stuff.
    // Push other objects on the stack as needed.
    ...


注意:如果内部有多个递归调用,并且想要保留调用的顺序,则必须以相反的顺序将它们添加到堆栈中:

foo(first);
foo(second);

必须替换为

stack.push(second);
stack.push(first);

编辑:文章Stacks and Recursion Elimination(或Article Backup link)详细介绍了这个主题。

【讨论】:

如果你用 Queue 子替换你的栈,那不就解决了颠倒添加顺序的问题吗? 我在纸上计算出来的,它们是两个不同的东西。如果你颠倒添加它们的顺序,它会让你像往常一样向前遍历,但你的遍历仍然是深度优先搜索。但是如果你现在把整个东西变成一个队列,你就是在做广度优先而不是深度优先的遍历。 我最近才以一般方式执行此操作,将我的节点访问函数(node)->() 替换为(node)->[actions],其中操作为() -> [actions]。然后在外面,你只需将一个动作/延续从堆栈中弹出,应用/执行它,将它返回的动作按相反的顺序推送到堆栈上并重复。偶然/复杂遍历,您只需在引用计数指针中捕获本地堆栈变量,然后在您的 thunk 中关闭,然后后续 thunk 可以取决于先前子遍历的结果等。 有时我们会避免递归以避免***。但是维护我们自己的栈也会导致***。那么我们为什么要用自己的栈来实现递归呢? @ZhuLi 如果我们使用new,我们可以在堆上而不是栈上创建一个对象。与栈不同,堆没有内存限制。见gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html【参考方案7】:

即使使用堆栈也不会将递归算法转换为迭代。普通递归是基于函数的递归,如果我们使用堆栈,那么它就变成了基于堆栈的递归。但它仍然是递归的。

对于递归算法,空间复杂度为 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) 基于数据的大小,但它显然是一种迭代算法。 @Ponkadoodle 是的。迭代和递归解决方案都需要 O(N) 空间和时间复杂度,因为递归只是用程序堆栈替换调用堆栈。但是仍然有一些原因可以将递归转换为迭代,其中之一是使用 CUDA 编程之类的东西将您的串行执行代码转换为并行代码。【参考方案8】:

只是消磨时间......一个递归函数

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的例子:)【参考方案9】:

似乎没有人解决递归函数在主体中多次调用自身的位置,并处理返回到递归中的特定点(即不是原始递归)。据说是every recursion can be turned into iteration,所以看来这应该是可以的。

我刚刚想出了一个如何做到这一点的 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();
            


        

【讨论】:

它真的很有用,我不得不编写迭代版本的递归,它会自动调用 n 次,感谢你的帖子,我做到了。 这必须是我见过的模拟调用堆栈递归的最佳示例,用于在方法中进行多个递归调用的情况。干得好。 您让我在“似乎没有人解决递归函数在正文中多次调用自身的位置,并处理返回到递归中的特定点”,然后我已经投票了。好的,现在我将阅读您的其余答案,看看我的过早投票是否合理。 (因为我迫切需要知道答案)。 @mydoghasworms - 这么久后回到这个问题, 花了一点时间才想起我在想什么。希望答案有所帮助。 我喜欢这个解决方案的想法,但它似乎让我感到困惑。我用python写了二叉树的简化版,也许它会帮助别人理解这个想法:gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac【参考方案10】:

递归只不过是从另一个函数调用一个函数的过程,只有这个过程是通过调用一个函数本身来完成的。正如我们所知,当一个函数调用另一个函数时,第一个函数保存其状态(其变量),然后将控制权传递给被调用函数。被调用的函数可以使用同名的变量来调用,例如 fun1(a) 可以调用 fun2(a)。 当我们进行递归调用时,没有任何新的事情发生。一个函数通过将相同类型和名称相似的变量(但显然存储在变量中的值不同,只有名称保持不变)传递给自己来调用自己。但是在每次调用之前,该函数都会保存其状态,并且这个保存过程会继续进行。保存是在堆栈上完成的。

现在堆栈开始发挥作用了。

所以如果你编写一个迭代程序,每次都将状态保存在堆栈中,然后在需要时从堆栈中弹出值,你就成功地将递归程序转换为迭代程序!

证明很简单,分析性强。

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

考虑一下,只需将深度优先搜索(在图上)递归程序转换为 dfs 迭代程序。

一切顺利!

【讨论】:

【参考方案11】:

一般来说,避免堆栈溢出的技术是用于递归函数的称为蹦床技术,被 Java 开发人员广泛采用。

但是,对于 C#,有一个小辅助方法 here 可以将您的递归函数转换为迭代,而无需更改逻辑或使代码难以理解。 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);

【讨论】:

【参考方案12】:

stacks and recursion elimination 文章捕获了在堆上外部化堆栈帧的想法,但没有提供简单且可重复的转换方式。下面是一个。

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

考虑这个递归代码:

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, stackitem 在未初始化的情况下不得推送。但是,是的,初始化为 0 会捕获错误。 为什么不将两个返回地址都设置为v.pop_back() 语句?【参考方案13】:

系统如何获取任何递归函数并使用堆栈执行它的粗略描述:

这是为了展示这个想法而没有细节。考虑这个可以打印出图节点的函数:

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 行继续使用局部变量 state node=A” - 使用 node=B 转到第 0 行。

要执行代码,系统会运行指令。当遇到函数调用时,系统将需要返回的信息推送到原来的位置,运行函数代码,当函数完成时,弹出需要去哪里继续的信息。

【讨论】:

【参考方案14】:

link 提供了一些解释,并提出了保持“位置”以便能够在多次递归调用之间到达确切位置的想法:

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

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

【讨论】:

【参考方案15】:

已关闭的question 的副本具有非常特定的数据结构:

该节点具有以下结构:

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 的数据链接结构。当前节点的孩子被重新排列,以便最后一个孩子采用所有其他孩子。然后可以删除当前节点,然后遍历可以迭代到剩余的孩子。

【讨论】:

【参考方案16】:

想想真正需要堆栈的东西:

如果我们将递归模式视为:

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 堆栈中的 4 个元素,而不是 Task 堆栈中的一个元素)。

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

【讨论】:

【参考方案17】:

有一种通用的方法可以将递归遍历转换为迭代器,方法是使用连接多个迭代器供应商的惰性迭代器(返回迭代器的 lambda 表达式)。见我的Converting Recursive Traversal to Iterator。

【讨论】:

【参考方案18】:

另一个使用堆栈将递归函数转换为迭代函数的简单而完整的示例。

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

【讨论】:

【参考方案19】:

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

给定这个函数,***s 对于较大的 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 function:

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

【讨论】:

【参考方案20】:

这是一个老问题,但我想添加一个不同的方面作为解决方案。我目前正在做一个项目,在这个项目中我使用了 C# 的洪水填充算法。通常,我一开始使用递归来实现这个算法,但很明显,它导致了堆栈溢出。之后,我将方法从递归更改为迭代。是的,它起作用了,我不再收到堆栈溢出错误。但这一次,由于我将洪水填充方法应用于非常大的结构,因此程序进入了无限循环。出于这个原因,我想到这个函数可能重新进入了它已经访问过的地方。作为对此的最终解决方案,我决定为访问点使用字典。如果该 node(x,y) 已经首次添加到堆栈结构中,则该 node(x,y) 将作为键保存在字典中。即使稍后尝试再次添加相同的节点,它也不会被添加到堆栈结构中,因为该节点已经在字典中。让我们看看伪代码:

startNode = pos(x,y)

Stack stack = new Stack();

Dictionary visited<pos, bool> = new Dictionary();

stack.Push(startNode);

while(stack.count != 0)
    currentNode = stack.Pop();
    if "check currentNode if not available"
        continue;
    if "check if already handled"
        continue;
    else if "run if it must be wanted thing should be handled"      
        // make something with pos currentNode.X and currentNode.X  
        
        // then add its neighbor nodes to the stack to iterate
        // but at first check if it has already been visited.
        
        if(!visited.Contains(pos(x-1,y)))
            visited[pos(x-1,y)] = true;
            stack.Push(pos(x-1,y));
        if(!visited.Contains(pos(x+1,y)))
            ...
        if(!visited.Contains(pos(x,y+1)))
            ...
        if(!visited.Contains(pos(x,y-1)))
            ...


【讨论】:

【参考方案21】:

TLDR

您可以比较下面的源代码,前后对比,以直观地理解该方法,而无需阅读整个答案。

我在处理非常大的文本块以生成后缀数组时遇到了一些多键快速排序代码的问题。由于所需的递归深度极深,代码将中止。通过这种方法,终止问题得到了解决。转换后,可以捕获某些作业所需的最大帧数,介于 10K 和 100K 之间,占用 1M 到 6M 内存。不是最佳解决方案,有更有效的方法来生成后缀数组。但无论如何,这是使用的方法。

方法

将递归函数转换为适用于任何情况的迭代解决方案的一般方法是模拟函数调用和调用返回期间本地编译代码使用的过程。

举一个需要一些复杂方法的例子,我们有多键快速排序算法。这个函数有三个连续的递归调用,每次调用后,从下一行开始执行。

函数的状态在栈帧中被捕获,栈帧被压入执行栈。当sort() 在自身内部被调用并返回时,调用时存在的堆栈帧被恢复。这样,所有变量都具有与调用前相同的值 - 除非它们被调用修改。

递归函数

def sort(a: list_view, d: int):
    if len(a) <= 1:
        return
    p = pivot(a, d)
    i, j = partition(a, d, p)
    sort(a[0:i], d)
    sort(a[i:j], d + 1)
    sort(a[j:len(a)], d)

采用这个模型,并模仿它,一个列表被设置为堆栈。在此示例中,元组用于模拟帧。如果这是用 C 编码的,则可以使用结构。数据可以包含在数据结构中,而不是一次只推送一个值。

重新实现为“迭代”

# Assume `a` is view-like object where slices reference
# the same internal list of strings.

def sort(a: list_view):
    stack = []
    stack.append((LEFT, a, 0))                  # Initial frame.

    while len(stack) > 0:
        frame = stack.pop()  

        if len(frame[1]) <= 1:                  # Guard.
            continue

        stage = frame[0]                        # Where to jump to.

        if stage == LEFT: 
            _, a, d = frame                     # a - array/list, d - depth.
            p = pivot(a, d)
            i, j = partition(a, d, p)
            stack.append((MID, a, i, j, d))     # Where to go after "return".
            stack.append((LEFT, a[0:i], d))     # Simulate function call.

        elif stage == MID:                      # Picking up here after "call"
            _, a, i, j, d = frame               # State before "call" restored.
            stack.append((RIGHT, a, i, j, d))   # Set up for next "return".
            stack.append((LEFT, a[i:j], d + 1)) # Split list and "recurse".

        elif stage == RIGHT:
            _, a, _, j, d = frame
            stack.append((LEFT, a[j:len(a)], d)

        else:
           pass

进行函数调用时,函数返回后从何处开始执行的信息包含在堆栈帧中。在此示例中,if/elif/else 块表示从调用返回后开始执行的点。在 C 中,这可以实现为 switch 语句。

在示例中,块被赋予标签;它们通过列表在每个块内的分区方式任意标记。第一个块“LEFT”在左侧拆分列表。 “MID”部分表示中间分割列表的块等。

使用这种方法,模仿呼叫需要两个步骤。首先,一帧被压入堆栈,这将导致在“调用”“返回”之后的当前块之后的块中继续执行。框架中的值指示要落入“调用”之后的循环中的哪个if/elif/else 部分。

然后“调用”帧被压入堆栈。对于此特定示例,在大多数情况下,这会将执行发送到第一个“LEFT”块。这是实际排序完成的地方,无论列表的哪个部分被拆分到那里。

在循环开始之前,推到函数顶部的主帧代表初始调用。然后在每次迭代中,都会弹出一个帧。框架中的“LEFT/MID/RIGHT”值/标签用于落入if/elif/else 语句的正确块中。该帧用于恢复当前操作所需变量的状态,然后在下一次迭代中弹出返回帧,将执行发送到后续部分。

返回值

如果递归函数返回一个自己使用的值,它可以像其他变量一样被对待。只需在堆栈帧中为其创建一个字段。如果“被调用者”正在返回一个值,它会检查堆栈以查看它是否有任何条目;如果是这样,则更新堆栈顶部框架中的返回值。对于这个you can check this other example 的例子,同样的递归到迭代转换的方法。

结论

像这样将递归函数转换为迭代函数的方法本质上也是“递归的”。代替进程堆栈用于实际的函数调用,另一个以编程方式实现的堆栈取而代之。

得到了什么?也许在速度上有一些边际改进。或者它可以作为绕过某些编译器和/或执行环境(堆栈指针命中保护页)施加的堆栈限制的一种方法。在某些情况下,可以减少压入堆栈的数据量。收益是否通过模仿我们通过递归实现自动获得的东西来抵消代码中引入的复杂性?

在排序算法的情况下,找到一种不使用堆栈来实现这个特定算法的方法可能具有挑战性,而且有很多迭代排序算法可用,而且速度要快得多。据说任何递归算法都可以迭代实现。当然...但是有些算法如果不经过修改就不能很好地转换,以至于它们不再是相同的算法。

仅仅为了转换递归算法而转换它们可能不是一个好主意。无论如何,不​​管怎样,上述方法是一种通用的转换方式,应该适用于几乎任何事情。

如果您发现您确实需要一个不使用自己的内存消耗堆栈的递归函数的迭代版本,最好的方法可能是废弃代码并使用学术文章中的描述编写自己的代码,或在纸上解决,然后从头开始编写代码,或其他从头开始的方法。

【讨论】:

以上是关于从递归到迭代的方法的主要内容,如果未能解决你的问题,请参考以下文章

迭代是人,递归是神(迭代与递归的总结:比较)

迭代 递归实例

迭代是人,递归是神(迭代与递归的总结:比较)

将函数从递归转换为迭代

迭代牛顿法到递归(Java)

JavaSE面试题——方法的递归与迭代