这是一个例子,而不是用文字解释。这是阶乘函数的Scheme版本:
(define (factorial x) (if (= x 0) 1 (* x (factorial (- x 1)))))
这是一个尾递归的factorial版本:
(define factorial (letrec ((fact (lambda (x accum) (if (= x 0) accum (fact (- x 1) (* accum x)))))) (lambda (x) (fact x 1))))
您将在第一个版本中注意到对事实的递归调用被送入乘法表达式,因此在进行递归调用时必须将状态保存在堆栈中。在尾递归版本中,没有其他S表达式等待递归调用的值,并且由于没有其他工作要做,因此不必将状态保存在堆栈中。通常,Scheme尾递归函数使用常量堆栈空间。
摘自本书 Lua编程 节目 如何进行正确的尾递归 (在Lua中,但也应该适用于Lisp)以及为什么它更好。
一个 尾巴电话 [tail recursion]是一种goto打扮 作为电话。当a时发生尾调用 函数将另一个调用为最后一个 行动,所以它没有别的办法。 例如,在以下代码中, 打电话给 g 是一个尾调: function f (x) return g(x) end 后 f 电话 g ,它没有别的 去做。在这种情况下,该计划 不需要返回调用 函数调用时的函数 结束。因此,尾调用后, 该计划不需要保留任何 有关呼叫功能的信息 在堆栈中。 ... 因为正确的尾调用不会 堆栈空间,没有限制 “嵌套”尾部调用的数量a 程序可以做。例如,我们可以 用any调用以下函数 数字作为参数;永远不会 溢出堆栈: function foo (n) if n > 0 then return foo(n - 1) end end ......正如我之前所说,尾部呼叫是一个 转发。因此,一个非常有用 应用适当的尾调用 Lua用于编程状态机。 这样的应用可以代表每个 以功能陈述;改变国家 是去(或打电话)具体的 功能。举个例子,让我们来吧 考虑一个简单的迷宫游戏。迷宫 有几个房间,每个房间最多 四扇门:北,南,东,和 西方。在每一步,用户输入一个 运动方向。如果有门 在那个方向,用户去 相应的房间;否则, 程序打印警告。目标是 从最初的房间到决赛 房间。 这个游戏是一个典型的状态机, 当前房间是哪个州。 我们可以用一个这样的迷宫来实现 每个房间的功能。我们用尾巴 打电话从一个房间搬到 另一个。一个有四个房间的小迷宫 可能看起来像这样: function room1 () local move = io.read() if move == "south" then return room3() elseif move == "east" then return room2() else print("invalid move") return room1() -- stay in the same room end end function room2 () local move = io.read() if move == "south" then return room4() elseif move == "west" then return room1() else print("invalid move") return room2() end end function room3 () local move = io.read() if move == "north" then return room1() elseif move == "east" then return room4() else print("invalid move") return room3() end end function room4 () print("congratulations!") end
一个 尾巴电话 [tail recursion]是一种goto打扮 作为电话。当a时发生尾调用 函数将另一个调用为最后一个 行动,所以它没有别的办法。 例如,在以下代码中, 打电话给 g 是一个尾调:
g
function f (x) return g(x) end
后 f 电话 g ,它没有别的 去做。在这种情况下,该计划 不需要返回调用 函数调用时的函数 结束。因此,尾调用后, 该计划不需要保留任何 有关呼叫功能的信息 在堆栈中。 ...
f
因为正确的尾调用不会 堆栈空间,没有限制 “嵌套”尾部调用的数量a 程序可以做。例如,我们可以 用any调用以下函数 数字作为参数;永远不会 溢出堆栈:
function foo (n) if n > 0 then return foo(n - 1) end end
......正如我之前所说,尾部呼叫是一个 转发。因此,一个非常有用 应用适当的尾调用 Lua用于编程状态机。 这样的应用可以代表每个 以功能陈述;改变国家 是去(或打电话)具体的 功能。举个例子,让我们来吧 考虑一个简单的迷宫游戏。迷宫 有几个房间,每个房间最多 四扇门:北,南,东,和 西方。在每一步,用户输入一个 运动方向。如果有门 在那个方向,用户去 相应的房间;否则, 程序打印警告。目标是 从最初的房间到决赛 房间。
这个游戏是一个典型的状态机, 当前房间是哪个州。 我们可以用一个这样的迷宫来实现 每个房间的功能。我们用尾巴 打电话从一个房间搬到 另一个。一个有四个房间的小迷宫 可能看起来像这样:
function room1 () local move = io.read() if move == "south" then return room3() elseif move == "east" then return room2() else print("invalid move") return room1() -- stay in the same room end end function room2 () local move = io.read() if move == "south" then return room4() elseif move == "west" then return room1() else print("invalid move") return room2() end end function room3 () local move = io.read() if move == "north" then return room1() elseif move == "east" then return room4() else print("invalid move") return room3() end end function room4 () print("congratulations!") end
所以你看,当你做一个递归调用时:
function x(n) if n==0 then return 0 n= n-2 return x(n) + 1 end
这不是尾递归,因为在进行递归调用之后,您仍然可以在该函数中执行操作(添加1)。如果输入一个非常高的数字,可能会导致堆栈溢出。
在 的 传统的递归 强> ,典型的模型是先执行递归调用,然后获取递归调用的返回值并计算结果。以这种方式,在每次递归调用返回之前,您不会得到计算结果。
在 的 尾递归 强> ,首先执行计算,然后执行递归调用,将当前步骤的结果传递给下一个递归步骤。这导致最后一个语句采用的形式 (return (recursive-function params)) 。 的 基本上,任何给定递归步骤的返回值与下一个递归调用的返回值相同 强> 。
(return (recursive-function params))
这样做的结果是,一旦准备好执行下一个递归步骤,就不再需要当前的堆栈帧了。这允许一些优化。实际上,使用适当编写的编译器,您应该永远不会有堆栈溢出 暗笑 尾递归调用。只需重复使用当前堆栈帧进行下一个递归步骤。我很确定Lisp会这样做。
递归函数是一个函数 的 自己打电话 强>
它允许程序员使用a编写高效的程序 的 最少量的代码 强> 。
缺点是他们可以 的 导致无限循环 强> 和其他意外的结果如果 的 写得不好 强> 。
我会解释两者 的 简单的递归函数和尾递归函数 强>
为了写一个 的 简单的递归函数 强>
从给定的例子:
public static int fact(int n){ if(n <=1) return 1; else return n * fact(n-1); }
从上面的例子
if(n <=1) return 1;
是退出循环的决定性因素
else return n * fact(n-1);
是否要进行实际处理
让我逐一打破任务,以便于理解。
如果我跑,让我们看看内部会发生什么 fact(4)
fact(4)
public static int fact(4){ if(4 <=1) return 1; else return 4 * fact(4-1); }
If 循环失败,所以它去了 else 环 所以它返回 4 * fact(3)
If
else
4 * fact(3)
在堆栈内存中,我们有 4 * fact(3)
的 代入n = 3 强>
public static int fact(3){ if(3 <=1) return 1; else return 3 * fact(3-1); }
If 循环失败,所以它去了 else 环
所以它返回 3 * fact(2)
3 * fact(2)
记得我们称之为```4 * fact(3)``
输出为 fact(3) = 3 * fact(2)
fact(3) = 3 * fact(2)
到目前为止堆栈已经存在 4 * fact(3) = 4 * 3 * fact(2)
4 * fact(3) = 4 * 3 * fact(2)
在堆栈内存中,我们有 4 * 3 * fact(2)
4 * 3 * fact(2)
的 代入n = 2 强>
public static int fact(2){ if(2 <=1) return 1; else return 2 * fact(2-1); }
所以它返回 2 * fact(1)
2 * fact(1)
记得我们叫了 4 * 3 * fact(2)
输出为 fact(2) = 2 * fact(1)
fact(2) = 2 * fact(1)
到目前为止堆栈已经存在 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
在堆栈内存中,我们有 4 * 3 * 2 * fact(1)
4 * 3 * 2 * fact(1)
的 代入n = 1 强>
public static int fact(1){ if(1 <=1) return 1; else return 1 * fact(1-1); }
If 循环是真的
所以它返回 1
1
记得我们叫了 4 * 3 * 2 * fact(1)
输出为 fact(1) = 1
fact(1) = 1
到目前为止堆栈已经存在 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
最后,结果 的 事实(4)= 4 * 3 * 2 * 1 = 24 强>
该 的 尾递归 强> 将会
public static int fact(x, running_total=1) { if (x==1) { return running_total; } else { return fact(x-1, running_total*x); } }
public static int fact(4, running_total=1) { if (x==1) { return running_total; } else { return fact(4-1, running_total*4); } }
If 循环失败,所以它去了 else 环 所以它返回 fact(3, 4)
fact(3, 4)
在堆栈内存中,我们有 fact(3, 4)
public static int fact(3, running_total=4) { if (x==1) { return running_total; } else { return fact(3-1, 4*3); } }
所以它返回 fact(2, 12)
fact(2, 12)
在堆栈内存中,我们有 fact(2, 12)
public static int fact(2, running_total=12) { if (x==1) { return running_total; } else { return fact(2-1, 12*2); } }
所以它返回 fact(1, 24)
fact(1, 24)
在堆栈内存中,我们有 fact(1, 24)
public static int fact(1, running_total=24) { if (x==1) { return running_total; } else { return fact(1-1, 24*1); } }
所以它返回 running_total
running_total
输出为 running_total = 24
running_total = 24
最后,结果 的 事实(4,1)= 24 强>
这意味着您可以简单地跳转到递归函数的顶部并继续执行,而不需要将指令指针推到堆栈上。这允许函数无限递归,而不会溢出堆栈。
我写了一篇 博客 在主题上发布,其中包含堆栈帧的图形示例。
这是Perl 5版本的 tailrecsum 前面提到的功能。
tailrecsum
sub tail_rec_sum($;$){ my( $x,$running_total ) = (@_,0); return $running_total unless $x; @_ = ($x-1,$running_total+$x); goto &tail_rec_sum; # throw away current stack frame }
使用常规递归,每个递归调用将另一个条目推送到调用堆栈。递归完成后,应用程序必须将每个条目一直弹回。
使用尾递归,根据语言,编译器可能能够将堆栈折叠为一个条目,因此您可以节省堆栈空间......大型递归查询实际上可能导致堆栈溢出。
基本上Tail递归可以优化为迭代。
递归意味着一个函数调用自身。例如:
(define (un-ended name) (un-ended 'me) (print "How can I get here?"))
Tail-Recursion表示结束函数的递归:
(define (un-ended name) (print "hello") (un-ended 'me))
看,最后一个未结束的函数(过程,在方案术语中)做的是调用自身。另一个(更有用)的例子是:
(define (map lst op) (define (helper done left) (if (nil? left) done (helper (cons (op (car left)) done) (cdr left)))) (reverse (helper '() lst)))
在辅助程序中,如果左边不是nil,它会做的最后一件事就是调用自己(在有些事情和cdr之后)。这基本上是您映射列表的方式。
尾递归具有很大的优势,解释器(或编译器,依赖于语言和供应商)可以优化它,并将其转换为等同于while循环的东西。事实上,在Scheme传统中,大多数“for”和“while”循环是以尾递归方式完成的(据我所知,没有for和while)。
我不是Lisp程序员,但我认为 这个 会有所帮助。
基本上它是一种编程风格,使得递归调用是你做的最后一件事。
考虑一个添加前N个整数的简单函数。 (例如。 sum(5) = 1 + 2 + 3 + 4 + 5 = 15 )。
sum(5) = 1 + 2 + 3 + 4 + 5 = 15
这是一个使用递归的简单JavaScript实现:
function recsum(x) { if (x===1) { return x; } else { return x + recsum(x-1); } }
如果你打电话 recsum(5) ,这是JavaScript解释器评估的内容:
recsum(5)
recsum(5) 5 + recsum(4) 5 + (4 + recsum(3)) 5 + (4 + (3 + recsum(2))) 5 + (4 + (3 + (2 + recsum(1)))) 5 + (4 + (3 + (2 + 1))) 15
请注意每个递归调用必须在JavaScript解释器开始实际执行计算总和之前完成。
这是同一函数的尾递归版本:
function tailrecsum(x, running_total=0) { if (x===0) { return running_total; } else { return tailrecsum(x-1, running_total+x); } }
这是您调用时将发生的事件序列 tailrecsum(5) ,(这实际上是 tailrecsum(5, 0) ,因为默认的第二个参数)。
tailrecsum(5)
tailrecsum(5, 0)
tailrecsum(5, 0) tailrecsum(4, 5) tailrecsum(3, 9) tailrecsum(2, 12) tailrecsum(1, 14) tailrecsum(0, 15) 15
在尾递归的情况下,对每个递归调用的评估, running_total 已更新。
注意:原始答案使用了Python中的示例。由于现代JavaScript解释器支持,因此已将其更改为JavaScript 尾调用优化 但Python解释器却没有。