【问题标题】:handling the tail part of a non-tail recursive function when converting it to an iterative function在将非尾递归函数转换为迭代函数时处理其尾部分
【发布时间】:2012-07-29 00:43:08
【问题描述】:

当使用您自己制作的堆栈将非尾递归函数转换为迭代函数时,处理递归调用(即尾)之后的代码部分的一般方法是什么?

以下函数应该探索迷宫中所有可能的路径,重新访问预先访问的路径以访问堆栈中的其他路径:

struct node{
    int id;
    bool free;
    int neighborNode[4];
    int toProcess;
} nodeMap[100];

void findPath(int current){
    visited[current] = true;
    int i;
    for (i=0; i < 4; i++){
       if(nodeMap[nodeMap[current].neighborNode[i]].free == true && visited[nodeMap[current].neighborNode[i]] == false && nodeMap[current].neighborNode[i] != -1){
        path[cc] = nodeMap[nodeMap[current].neighborNode[i]].id;
        cc++;
        findPath(nodeMap[current].neighborNode[i]);
        path[cc] = nodeMap[current].id;
        cc++;
        }
    }
}

代码的递归部分很容易转换为迭代(我使用了一个字段toProcess 来模拟循环的索引,因为它没有保存在堆栈中,并且是处理所有孩子所必需的):

void findPath(){
    if (isEmpty())
        return;
    else {
        node temp = pop();
        visited[temp.id] = true;
        if (temp.toProcess < 3) {
            temp.toProcess++;
            push(temp);
            temp.toProcess--;
        }
        if(nodeMap[temp.neighborNode[temp.toProcess]].free == true && visited[temp.neighborNode[temp.toProcess]] == false && temp.neighborNode[temp.toProcess] != -1){
            path[cc] = nodeMap[temp.neighborNode[temp.toProcess]].id;
            cc++;
            push(nodeMap[temp.neighborNode[temp.toProcess]]);
        }
    }
}

但是算法向后移动以重新访问先前看到的节点以探索其他可能的路径(尾部)的部分,即path[cc] = nodeMap[current].id; & cc++; 似乎不适合该方法的迭代版本! 有一个通用的方法吗?还是每个案例都不一样?无论如何,您有什么建议如何在这种情况下实现尾部?

【问题讨论】:

  • 如果对这个问题投反对票的人解释了原因,那就太好了。我给它 +1 是因为它很有趣,尽管这个例子本来可以减少。

标签: c algorithm methods methodology


【解决方案1】:

使用尾递归函数,堆栈解决方案既好又简单,但就像在您的示例中一样,由于您是在递归调用之后执行某些操作,因此您需要找出一种在调用结束后执行这些操作的方法。

以下是可能的解决方案:

struct stack_element
{
    ... your_stuff...
    bool expanded;
};

在代码中:

stack_element e;
... fill e
e.expanded = false;
push(e);
while (!empty())
{
    e = pop();
    if (e.expanded)
    {
        ... do the stuff that was supposed to be done
        ... after e and all its children are processed
    }
    else
    {
        e.expanded = true;
        push(e);               // push e, so it would be visited again
                               // once all children are processed
        for (every child of e)
            if (they met the conditions)
            {
                ... do the stuff before
                stack_element child;
                ... fill child
                child.expanded = false;
                push(child);
            }
    }
}

这基本上是,访问每个节点两次。一次在展开时,此时您在递归调用 before 执行内容,另一次在其所有子级完成处理后,此时您在 after 执行内容递归调用。

请注意,您可能需要保存一些状态,例如 cccurrent 以及节点,以便您能够正确执行 if (e.expanded) 部分。


建议:使用for循环,就像你在递归方法中所做的那样,这比使用toProcess更清晰。


在您的情况下,对孩子的一个分支执行会影响访问或不访问其他分支,您可以遵循以下方法:

每次得到一个节点,检查它是否满足必要条件。如果是,则在调用该节点之前进行处理。然后像以前一样,再次推送它,以便再次访问它并完成后处理。这样,每次你只是推孩子,然后再决定他们好不好:

struct stack_element
{
    ... your_stuff...
    bool expanded;
    ... save also cc, current and i
};

stack_element e;
... fill e
e.expanded = false;
push(e);
while (!empty())
{
    e = pop();
    if (e.expanded)
    {
        ... do the stuff that was supposed to be done
        ... when function called with e is returning and
        ... after the function returns to parent
    }
    else if (conditions for are met)
    {
        ... do the stuff that was supposed to be done before
        ... e is recursively called and at the beginning of the
        ... function
        e.expanded = true;
        push(e);               // push e, so it would be visited again
                               // once all children are processed
        for (every child of e, in reverse order)
        {
            stack_element child;
            ... fill child
            child.expanded = false;
            push(child);
        }
    }
}

查看确切代码后,这是转换后的版本:

struct stacknode{
    int id;
    int parent;
    bool free;
    bool expanded;
};

int cc = 0;

void findPath2(int current){

    // special case for the first call:
    visited[current] = true;

    // expand the first node, because when the first node is popped,
    // there was no "stuff before recursion" before it.
    int i;
    for (i=3; i >= 0; --i){
        // Put the neighbors on the stack so they would be inspected:
        stacknode child;
        child.id = nodeMap[current].neighborNode[i];
        child.parent = current;
        child.free = nodeMap[child.id].free;
        child.expanded = false;
        push(child);
    }

    while (!isEmpty())
    {
        stacknode cur = pop();
        if (cur.expanded == true)
        {
            // Now, it's like a return from a recursive function
            // Note: cur.id will be nodeMap[current].neighborNode[i] because that was the value the function was called with
            // Stuff at the end of the function:
            path[0]=current;    // Note: this is kind of absurd, it keeps getting overwritten!
            // Stuff after recursion:
            cc++;  // note that path[0] is reserved for first node, so you should first increment cc, then set path[cc]
            path[cc] = nodeMap[cur.parent].id;  // nodeMap[current].id: current was the parent of
                                // nodeMap[current].neighborNode[i] for which the function was called
        }
        else
        {
            // Now, it's like when the recursive function is called (including stuff before recursion)
            // Note: cur.id will be nodeMap[current].neighborNode[i] because that was the value the function was called with
            // Check whether child was supposed to be added:
            if(cur.id != -1 && nodeMap[cur.id].free == true && visited[cur.id] == false)
                // Node: Put cur.id != -1 in the beginning. Otherwise, you would possibly check
                // nodeMap[-1] and visited[-1] which is not nice
            {
                cur.expanded = true;
                push(cur);
                // Stuff before recursion:
                cc++;
                path[cc] = nodeMap[cur.id].id;      // in cur.id, nodeMap[current].neighborNode[i] was stored
                // Stuff at the beginning of function call:
                visited[cur.id] = true;
                // The body of the function:
                for (i=3; i >= 0; --i){
                    // Put the neighbors on the stack so they would be inspected:
                    stacknode child;
                    child.id = nodeMap[cur.id].neighborNode[i];
                    child.parent = cur.id;
                    child.free = nodeMap[child.id].free;
                    child.expanded = false;
                    push(child);
                }
            }
        }
    }
    // end of special case for first call:
    path[0] = current;
}

请注意,它开始变得复杂的原因是cc 的存在,它是一个全局变量。如果你有一个不使用全局变量的递归函数,那么转换会更简单。

【讨论】:

    【解决方案2】:

    将递归转换为迭代的一般方法是使用一种事件循环,该循环从堆栈中重复弹出事件并执行其处理程序。我先写一些和你的递归函数类似的伪代码,这样转换会更容易理解:

    recurse (node)
    {
        for (i = 0; i < 4; i++) {
            if (condition(node, i)) {
                recurse(next_node(node, i));
            }
        }
    }
    

    我们现在要做的是将这个递归函数拆分为一系列操作 - 但是,我们没有明确地编写它,而是将每个操作定义为一个事件及其对应的事件处理程序,以防之后还有更多工作要做此操作,处理程序将在执行其操作之前将进一步的工作推送到堆栈。

    my_iterative_function ()
    {
        // build the event stack and push the initial event
        EventStack stack;
        stack.push(NewRecurseEvent(node = first_node));
    
        // pop and execute events
        while (!stack.isEmpty()) {
            Event ev = stack.pop();
            ev.executeHandler();
        }
    }
    
    RecurseEventHandler (node)
    {
        // We are at the beginning of the "recurse" function.
        // Push iteration 0 (we could optimize this and just do it here).
        stack.push(NewIterationEvent(node = node, i = 0));
    }
    
    IterationEventHandler (node, i)
    {
        // Unless this is the last iteration, push the next iteration
        // to be executed after this iteration is complete. It will work
        // with the same node, but 'i' one greater.
        if (i < 3) {
            stack.push(NewIterationEvent(node = node, i = i + 1));
        }
    
        // do the work of this iteration
        if (condition(node, i)) {
            # Initiate the recursion by pushing.
            # It is crutial to understand that the event loop will pop this event,
            # and all events pushed by it, recursively, before
            # popping the continuation event we pushed above.
            # That is, it will behave just like the recursive variant.
            stack.push(NewRecurseEvent(node = next_node(node, i)));
        }
    }
    

    这取决于语言如何实际实现这个伪代码。在 C 中,事件对象可以是一个标记的联合,它包含各种事件处理程序的参数,并且您可以让事件循环直接执行事件处理程序操作:

    struct event {
        int type; // RECURSE_EVENT or ITERATION_EVENT
        union {
            struct {
                node node;
            } recurse_event;
            struct {
                node node;
                int i;
            } iteration_event;
        };
    };
    
    while (!stack.isEmpty()) {
        struct event ev = stack.pop();
        switch (ev.type) {
            case RECURSE_EVENT: {
                node n = ev.recurse_event.node;
                ...
            } break;
            case ITERATION_EVENT: {
                node n = ev.iteration_event.node;
                int i = ev.iteration_event.i;
                ...
            } break;
        }
    }
    

    由于递归调用后面的部分,您的实际递归函数比我的简单示例更复杂,因此您将需要更多事件来处理它。例如,您可以添加另一种事件来处理后递归步骤:

    IterationEventHandler (node, i)
    {
        if (i < 3) {
            stack.push(NewIterationEvent(node = node, i = i + 1));
        }
    
        if (condition(node, i)) {
            // This PostEvent will be executed after the recursive work,
            // but before the next iteration pushed above.
            stack.push(NewPostEvent(node = node, i = i));
            stack.push(NewRecurseEvent(node = next_node(node, i)));
        }
    }
    
    PostEventHandler (node, i)
    {
        // do your post-recursion work here
    }
    

    如果您考虑到在查看单个事件处理程序时,很容易理解这一点,所有工作都将按照它被推送的相反顺序执行。例如,在上面的IterationEventHandler,首先将执行递归,然后执行递归后步骤,最后开始下一个迭代步骤(如果有的话)。更简单地说,每个 push 都可以理解为一个可能递归的函数调用,只不过这些调用是自下而上执行的。

    请注意,这是一种通用方法。可能构建一个编译器,将任何递归函数转换为这样的事件循环。此外,这种方法不仅是将递归转换为迭代的优雅方式; “堆栈事件循环”的想法在事件驱动软件的设计中非常实用,特别是如果您还能够将已推送但尚未执行的事件出列。我已经在回复this question 时解释了这一点。

    【讨论】:

      猜你喜欢
      • 2021-06-18
      • 2016-01-06
      • 2019-09-14
      • 1970-01-01
      • 2016-04-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多