【问题标题】:Recursive function to loop and stack循环和堆栈的递归函数
【发布时间】:2014-01-17 15:10:45
【问题描述】:

众所周知,所有递归函数都可以只使用一个循环和一个堆栈来编写。虽然这种转换可以被批评为丑陋或可读性差,但它的主要用途显然是避免破坏堆。

有很自然的方法可以将简单的递归函数转换为循环。例如,使用累加器进行简单的尾递归消除。到目前为止,我还没有看到这个问题的明确答案。

至少对我来说,有时将这样的递归函数转换为循环似乎是黑魔法,提供了一个堆栈。例如,考虑为

编写一个非递归版本
f(n) = n,                    if n <= 1
f(n) = f(n-1) + f(n-2) + 1,  if n > 1

这个问题的核心是:

  • 是否有一种清晰的通用方法可以将递归函数转换为循环+堆栈?

【问题讨论】:

  • 如果你的例子被实现,它不会终止。
  • 哦,对不起。这就是斐波那契 + 1 函数。我的意思是 f(n-1) 而不是 f(n+1),我编辑了。谢谢你的建议!
  • 话虽如此,我不确定是否有通用策略,因为它在很大程度上取决于两件事:递归(通过参数)传递哪些信息以及从递归返回哪些信息(通过返回值)。它还取决于有多少递归调用。因此,可能存在转换算法,但它并非易事。
  • "...所有递归函数都可以编写...单循环和堆栈"可能有点夸大其词,但我不确定。我想像int f(int x, int y) { return f(x/2, y) * f(f(y-3, x)-f(y, x)) / f(y, x*f(x-1, y)/f(x-f(x-1, y), y-1)); } 这样的东西可能会被重写为一个循环,但我不想这样做......然后是相互递归的情况,比如f() 调用g() 调用f(),等等开,即使有更大的周期...
  • 是的,我确信它确实存在。编译器所做的就是一个例子。我想通过精确模拟编译器的工作来解决这个问题的一种方法,但我认为有一种更简单的方法。

标签: loops recursion


【解决方案1】:

可行性(100%)

here,我们知道任何递归函数都可以转换为迭代(进入循环),但您需要自己使用堆栈来保持状态。


如何做到这一点(一般):

您可以查看文章 How to replace recursive functions using stack and while-loop to avoid the stack-overflow,其中提供了有关如何将递归函数转换为堆栈和 while 循环的示例和步骤(10 个步骤/规则)。实例见下节。


示例

以下面的递归函数为例:

// Recursive Function "First rule" example
int SomeFunc(int n, int &retIdx)
{
    ...
        if(n>0)
        {
            int test = SomeFunc(n-1, retIdx);
            test--;
            ...
                return test;
        }
        ...
            return 0;
} 

应用文中介绍的10条规则/步骤后(详情见cmets),你会得到:

// Conversion to Iterative Function
int SomeFuncLoop(int n, int &retIdx)
{
    // (First rule)
    struct SnapShotStruct {
        int n;        // - parameter input
        int test;     // - local variable that will be used 
        //     after returning from the function call
        // - retIdx can be ignored since it is a reference.
        int stage;    // - Since there is process needed to be done 
        //     after recursive call. (Sixth rule)
    };
    // (Second rule)
    int retVal = 0;  // initialize with default returning value
    // (Third rule)
    stack<SnapShotStruct> snapshotStack;
    // (Fourth rule)
    SnapShotStruct currentSnapshot;
    currentSnapshot.n= n;          // set the value as parameter value
    currentSnapshot.test=0;        // set the value as default value
    currentSnapshot.stage=0;       // set the value as initial stage
    snapshotStack.push(currentSnapshot);
    // (Fifth rule)
    while(!snapshotStack.empty())
    {
        currentSnapshot=snapshotStack.top();
        snapshotStack.pop();
        // (Sixth rule)
        switch( currentSnapshot.stage)
        {
        case 0:
            // (Seventh rule)
            if( currentSnapshot.n>0 )
            {
                // (Tenth rule)
                currentSnapshot.stage = 1;            // - current snapshot need to process after
                //     returning from the recursive call
                snapshotStack.push(currentSnapshot);  // - this MUST pushed into stack before 
                //     new snapshot!
                // Create a new snapshot for calling itself
                SnapShotStruct newSnapshot;
                newSnapshot.n= currentSnapshot.n-1;   // - give parameter as parameter given
                //     when calling itself
                //     ( SomeFunc(n-1, retIdx) )
                newSnapshot.test=0;                   // - set the value as initial value
                newSnapshot.stage=0;                  // - since it will start from the 
                //     beginning of the function, 
                //     give the initial stage
                snapshotStack.push(newSnapshot);
                continue;
            }
            ...
                // (Eighth rule)
                retVal = 0 ;

            // (Ninth rule)
            continue;
            break; 
        case 1: 
            // (Seventh rule)
            currentSnapshot.test = retVal;
            currentSnapshot.test--;
            ...
                // (Eighth rule)
                retVal = currentSnapshot.test;
            // (Ninth rule)
            continue;
            break;
        }
    }
    // (Second rule)
    return retVal;
} 

顺便说一句,上面的文章是CodeProject的Prize winner in Competition &lt;Best C++ article of July 2012&gt;,所以应该是可信的。 :)

【讨论】:

    【解决方案2】:

    是的,但解决方案不会比递归解决方案好多少,除了可能会减少堆栈溢出的机会。

    通过查看与斐波那契数列类似的序列,除了 n = (0,1) 之外的每一轮的结果都加 1,您会看到一个模式。

    0 1 2 4 7 12 20 
        \  \___ ...\____
        0+1+1  1+2+1   7+12+1
    

    由于整个生成依赖于前两个数字,因此您可以在循环中使用两个变量来表示该数字,而您根本不需要堆栈。

    //intentional generic algol dialect
    int modfib (int n)
    {
        if( n <= 1 )
          return n;
        n = n - 2;
        int a=2;
        int b=4;
        int tmp;
        while( n-- > 0 )
        {
            tmp=a;
            a=b;
            b = tmp + b +1;
        }
        return a;
    }
    

    在编译器知道这样做之前,我们仍然需要人工来优化代码。

    【讨论】:

    • 这是一个创造性的解决方案,但这正是我想要避免的。我试图通过添加 1 来加强这种重复,但重点是 如果这个 f(n) 太难我找不到任何模式,我有很多参数、局部变量、循环、递归调用和其他函数调用? 给定递归算法,是否可以将其重写为非递归的等效版本?再次感谢您的创造性解决方案。
    • @CássioJandirPagnoncelli Nop。我所做的整个转换都是基于数学序列的,编译器无法做到这一点。使用堆栈 + 循环的重写实际上是递归,它将保持 O(2^n),因此当手动重写的迭代循环是 O(n) 时它会同样有效
    • 是的,在这个意义上你是对的。在我提供的函数中,有 O(1.61...^n) 函数调用,而在你的函数中,由于优化,它可以在 O(n) 循环中执行,但这种优化更接近于要求编译器重写斐波那契动态编程比适当的、可行的编译器优化。好吧,我试图解决的问题不是正确地解决该递归问题——实际上它有一个 O(1) 解决方案——而是将一个普通的递归函数转换为一个循环 + 循环算法。
    猜你喜欢
    • 2023-03-27
    • 2014-12-10
    • 2018-09-24
    • 2017-09-12
    • 2014-06-12
    • 1970-01-01
    • 2017-10-20
    • 2020-08-27
    相关资源
    最近更新 更多