可能重复:
Are there problems that cannot be written using tail recursion?
根据我的理解,尾递归是一种优化,当递归调用不需要来自它将发送垃圾邮件的递归调用的信息时,可以使用它。
是否有可能使用尾递归实现所有递归函数?那么像DFS这样的东西,你需要最内层的孩子在父母之前返回?
答案 0 :(得分:11)
这完全取决于你的要求。
如果你想把所有函数保存为具有相同签名的函数(没有可变状态),那么没有。最明显的例子是quicksort,其中两个调用都不能是尾调用。
如果你能以各种方式修改功能,那么是的。有时局部修改就足够了 - 通常你可以添加一个“累加器”来构建一些返回的表达式,但是,如果结果涉及非交换操作,那么你需要小心(例如,当天真地构建链表时,订单相反)或者您可以添加堆栈。
或者,您可以对整个程序进行全局修改,其中每个函数都将包含未来操作的函数作为额外参数。这是继续通过Pete is talking about。
如果您是手工工作,那么本地修改通常相当容易。但是如果你正在进行自动重写(例如在编译器中),那么采用全局方法(它需要更少的“智能”)就更简单了。
答案 1 :(得分:5)
是和否。
是的,与其他控制流机制一起使用(例如,继续传递),您可以将任意控制流表示为尾递归。
不,除非你用其他控制流机制补充尾递归,否则不可能将所有递归表示为尾递归。
答案 2 :(得分:2)
使用延续传递可以将所有程序重写为尾调用。只需在尾调用中添加一个参数,表示当前执行的continuation。
任何转动完整语言执行与继续传递提供相同的转换 - 为非尾调用返回的程序和输入参数创建哥德尔数,并将其作为参数传递给尾调用 - 尽管显然环境这是通过延续,共同例程或其他一流构造为您完成的,这使得它变得更加容易。
CPS用作编译器优化,我之前使用延续传递编写了解释器。 scheme programming language旨在允许以这样的方式实现它,其中包括尾部调用优化和第一类延续的标准中的要求。
答案 3 :(得分:2)
我不知道所有递归函数是否可以重写为尾递归,但其中很多都可以。一种标准方法是使用累加器。例如,可以编写阶乘函数(在Common Lisp中),如下所示:
(defun factorial (n)
(if (<= n 1)
1
(* n (factorial (1- n)))))
这是递归的,但不是尾递归。它可以通过添加累加器参数来进行尾递归:
(defun factorial-accum (accum n)
(if (<= n 1)
accum
(factorial-accum (* n accum) (1- n))))
可以通过将累加器设置为1来计算因子。例如,3的阶乘是:
(factorial-accum 1 3)
但是,使用这样的方法是否可以将所有递归函数重写为尾递归函数并不清楚。但肯定有很多功能。
答案 4 :(得分:2)
递归算法是一种根据Divide&amp ;;征服策略,解决每个中间子问题产生0,1个或更多新的较小子问题。如果这些子问题以LIFO顺序求解,则会得到经典的递归算法。
现在,如果已知您的算法在每一步只产生0或1个子问题,则可以通过尾递归轻松实现此算法。实际上,这种算法可以很容易地重写为迭代算法并通过简单的循环来实现。 (不用说,尾递归只是另一种不太明确的实现迭代的方法。)
这种递归算法的教科书示例是阶乘计算的递归方法:计算n!
首先需要计算(n-1)!
,即在每个递归步骤中只发现 1 较小的子问题。这个属性使得将因子计算算法变为真正的迭代算法(或尾递归算法)变得如此容易。
但是,如果您知道在一般情况下算法的每一步生成的子问题数量都超过1,那么您的算法基本上递归。它不能重写为迭代算法,不能通过尾递归实现。任何以迭代或尾递归方式实现这种算法的尝试都需要额外的非常数大小的LIFO存储来存储“待定”子问题。这样的实现尝试将通过手动实现递归来简单地模糊算法的不可避免的递归性质。
例如,诸如遍历具有父 - >子链接(并且没有子 - >父链接)的二叉树这样的简单问题是基本上递归的问题。它不能通过尾递归算法来完成,它不能通过迭代算法来完成。
答案 5 :(得分:2)
是的,你可以。转换通常涉及明确地维护必要的信息,否则这些信息将在运行时隐藏地分布在执行堆栈的调用帧中。
就这么简单。无论运行时系统在执行过程中隐含的是什么,我们都可以自己明确地做。这里没有什么大不了的。 PC由硅,铜和钢制成。
将DFS实现为具有要处理的状态/位置/节点的显式队列的循环是微不足道的。它实际上是这样定义的--DFS用来自它的所有弧替换队列中弹出的第一个条目; BFS将这些弧添加到队列的末尾。
continuation-passing style transformation 将程序中的所有函数调用作为尾调用完成。这是一个简单的生活现实。使用的延续将增长和缩小,但调用都将是尾调用。
我们可以进一步了解继续中的进程传播状态,作为堆上显式维护的数据。最终实现的是解释和实现,将堆栈上的隐式内容移动到堆中的显式内容中,简化并揭开控制流的神秘面纱。
答案 6 :(得分:-1)
不能仅在具有一次递归调用的调用时“自然地”完成。对于两个或多个递归调用,您当然可以自己模拟堆栈帧。但是在优化记忆的意义上,它将非常难看并且实际上不会是尾递归的。
尾递归的关键是你不想回到父堆栈。因此,只需将该信息传递给子堆栈,它可以完全替换父堆栈,而不是堆栈增长。