【问题标题】:Visit a tree or graph structure using tail recursion使用尾递归访问树或图结构
【发布时间】:2012-01-23 10:54:52
【问题描述】:

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

伪代码:

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

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

}

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

尾递归基本上是在:

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

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

我的问题是:

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

这可能吗?

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

【问题讨论】:

  • 您使用哪种语言?

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


【解决方案1】:

所以,事实上,您可以总是将函数重构为尾递归......大多数使用其他语言编程的人将使用延续来有效地对其进行编码。但是,我们在这里更具体地研究 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);
}

而且,它为您提供了一个尾递归函数,该函数将被编译器很好地优化。尽情享受吧!

【讨论】:

  • 很好的答案!任何原始递归函数都可以翻译成tail rec 是真的吗?
  • 可以,但需要一些费用。因为,您需要继承保留在堆栈中的所有信息。因此,有些问题更适合转换为原始递归,而有些问题则只是地狱。
  • 如果父母需要孩子的数据怎么办?即对每个孩子调用递归函数,当每个孩子完成时,对结果做一些事情。就像在图表中寻找桥梁一样。
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

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

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

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

    【讨论】:

    • @amit:嗯,你说的完全错了……你总是可以重构为尾递归(尤其是当你手头有一个迭代版本时)。
    猜你喜欢
    • 2012-07-15
    • 1970-01-01
    • 2012-10-22
    • 2016-03-13
    • 1970-01-01
    • 2015-02-10
    • 1970-01-01
    • 1970-01-01
    • 2015-02-14
    相关资源
    最近更新 更多