使用尾递归访问树或图结构

时间:2012-01-23 10:54:52

标签: c++ algorithm graph tree tail-recursion

假设我有一个以递归方式访问的结构。

伪代码:

visit(node n)
{
    if (n == visited)
         return;

    //do something
    setVisited(n);
    foreach child_node in n.getChildren(){
        visit(child_node);
    }

}

根据这个thread尾递归可能发生在:

  

尾递归基本上是在:

     
      
  • 只有一个递归调用
  •   
  • 该调用是函数
  • 中的最后一个语句   

在上面的伪代码中,递归调用是最后一个语句,但由于调用发生在循环内,因此存在多个递归调用。 我想编译器无法检测到尾递归。

我的问题是:

无论如何重构上面的代码,以使其尾递归? 我正在寻找一种不会删除递归的解决方案,如果有的话(例如,我不想使用堆栈来模拟递归并将其转换为迭代函数)。

这可能吗?

如果语言相关,我正在使用C ++。

3 个答案:

答案 0 :(得分:2)

尾递归是模仿没有堆栈[或任何其他数据结构]的循环,即O(1)额外空间。

当前的问题AFAIK,树/图遍历[假设每个节点中没有parent字段]无法在如此复杂的情况下完成[O(n)时间,O(1)空间]因此不能用单个循环完成,没有堆栈。因此,不可能进行尾递归重构。

编辑:问题可以在O(1)空间中解决,但是O(n ^ 2)时间[这是双循环],如this post中所示。< / p>

答案 1 :(得分:2)

所以,实际上你可以总是重构一个函数,使其成为尾递归...大多数用其他语言编程的人都会使用continuation来高效编码。但是,我们更具体地看一下C / C ++语言,所以我假设只是通过编写函数本身来解决这个问题(我的意思是没有在语言中添加通用的延续框架)。

让我们假设您的函数的迭代版本应该如下所示:

void iterative() {
  while (cond)
    dosomething()
}

然后,将其转换为尾递归函数只需编写:

void tailrecursive() {
  if (!cond)
    return;

  dosomething();
  tailrecursive();
}

大多数情况下,您需要通过&#39; &#39;递归调用添加了一些以前没用的额外参数。在您的特定情况下,您有一个预订树遍历:

void recursive_preorder(node n) {
  if (n == visited)
    return;

  dosomething(n);
  foreach child_node in n.getChildren() {
    recursive_preorder(child_node);
  }
}

迭代等价物需要引入一个堆栈来记住资源管理器节点(因此,我们添加了推/弹操作):

void iterative_preorder(node n) {
  if (n == visited)
    return;

  stack worklist = stack().push(n);
  while (!worklist.isEmpty()) {
    node n = worklist.pop()
    dosomething (n);
    foreach child_node in n.getChildren() {
        worklist.push(child_node);
    }
  }
}

现在,采用preorder树遍历的这个迭代版本并将其转换为尾递归函数应该给出:

void tail_recursive_preorder_rec(stack worklist) {
  if (!worklist.isEmpty()) {
    node n = worklist.pop()
    dosomething (n);
    foreach child_node in n.getChildren() {
        worklist.push(child_node);
    }
  }
  tail_recursive_preorder_rec(worklist)
}

void tail_recursive_preorder (node n) {
  stack worklist = stack().push(n);
  tail_recursive_preorder_rec(worklist);
}

并且,它为您提供了一个尾递归函数,它将由编译器很好地优化。享受!

答案 2 :(得分:1)

我认为你不能。尾递归基本上是快捷方式的各种功能调用程序,以避免在调用返回调用函数后也立即返回的不必要的代价。但是,在这种情况下,在任何给定的递归级别,您(可能)进行多次调用,因此您需要能够从这些调用返回,然后再创建另一个,这不是尾递归方案。你最希望的是最后一个被优化为尾调用,但是我怀疑编译器是否可以检测到它,因为正如你所说的那样,它在循环中并且你在迭代的数据在编译时是未知的。 / p>

我想不出一种方法可以改变算法以使其具有尾递归性 - 你总是会有多个孩子的潜力,这会让它变得混乱。