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

Visit a tree or graph structure using tail recursion

本文关键字:结构 访问 尾部 递归      更新时间:2023-10-16

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

伪码:

visit(node n)
{
    if (n == visited)
         return;
    //do something
    setVisited(n);
    foreach child_node in n.getChildren(){
        visit(child_node);
    }
}

根据这个线程尾递归可以在以下情况下发生:

尾递归基本上是当:

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

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

我的问题是:

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

这可能吗?

如果语言是相关的,我使用的是C++。

因此,事实上,您可以始终重构函数,使其为尾部递归。。。大多数用其他语言编程的人都会使用continuation来高效地对其进行编码。但是,我们在这里更具体地研究C/C++语言,所以我假设只需对函数本身进行编码就可以消除这种情况(我的意思是不向该语言添加通用的延续框架)。

让我们假设你有一个迭代版本的函数,应该看起来像这样:

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

然后,只需编写以下内容即可将其转换为尾部递归函数:

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

大多数时候,您需要向递归调用传递一个"状态",该调用会添加一些以前没有用的额外参数。在您的特定情况下,您有一个预订单树遍历:

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

现在,采用这个迭代版本的预订单树遍历,并将其转化为尾部递归函数,应该会给出:

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

并且,它为您提供了一个尾部递归函数,编译器将对其进行很好的优化。享受

我认为你做不到。尾递归基本上缩短了函数调用过程的各个部分,以避免在调用返回后调用函数也立即返回的情况下产生不必要的开销。然而,在这种情况下,在任何给定的递归级别上,您都会(可能)进行多个调用,因此您需要能够从这些调用中返回,然后再进行另一个调用,这不是尾部递归场景。你最希望的是最后一个被优化为尾部调用,但我怀疑编译器能否检测到这一点,因为正如你所说,它在循环中,而你迭代的数据在编译时是未知的。

我想不出一种方法来改变算法,使其尾部递归——你总是会有多个孩子的潜力,这会把它搞砸。

尾递归是在没有堆栈[或任何其他数据结构]的情况下实例化循环,即O(1)额外的空间。

树/图遍历的AFAIK问题[假设每个节点中没有parent字段]不能以这样的复杂度[O(n)时间,O(1)空间]来完成,因此不能用单个循环、没有堆栈来完成。因此,不可能进行尾部递归重构。

编辑:这个问题可以在O(1)空间中解决,但需要O(n^2)时间[这是双循环],如本文所示。