【发布时间】:2011-05-14 01:59:17
【问题描述】:
我现在使用了许多递归函数,但仍然无法理解这样一个函数的确切工作原理(我熟悉第二行(即| n==0 = 1)但对最后一行不太熟悉(即| n>0 = fac (n-1) * n))。
fac :: Int -> Int
fac n
| n==0 = 1
| n>0 = fac (n-1) * n
【问题讨论】:
标签: haskell recursion factorial
我现在使用了许多递归函数,但仍然无法理解这样一个函数的确切工作原理(我熟悉第二行(即| n==0 = 1)但对最后一行不太熟悉(即| n>0 = fac (n-1) * n))。
fac :: Int -> Int
fac n
| n==0 = 1
| n>0 = fac (n-1) * n
【问题讨论】:
标签: haskell recursion factorial
Recursive algorithms 与mathematical induction 密切相关。也许研究其中一个会帮助您更好地理解另一个。
在使用递归时需要牢记两个关键原则:
归纳步骤通常是最困难的部分,因为它假设它所依赖的所有内容都已正确计算。实现这一信念的飞跃可能很困难(至少我花了一段时间才掌握它),但这只是因为我们的函数有先决条件;必须指定这些先决条件(在这种情况下,n 是一个非负整数),以便归纳步骤和基本情况始终为真。
Base Case 有时也很困难:比如说,你知道阶乘 N! 是 N * (N-1)!,但是你如何处理阶梯上的第一步呢? (在这种情况下,很容易定义0! := 1。这个明确的定义为您提供了一种终止归纳步骤的递归应用的方法。)
您可以在此函数中看到您的类型规范和保护模式提供了保证 Inductive Step 可以一遍又一遍地使用的前提条件,直到它到达基本案例 n == 0。如果不能满足先决条件,归纳步骤的递归应用将无法达到基本案例,并且您的计算将永远不会终止。 (嗯,它会在内存用完时。:)
一个复杂的因素,特别是对于函数式编程语言,是非常强烈的希望重写所有“简单”递归函数,就像你在这里一样,使用Tail Calls 或尾递归的变体。
因为这个函数调用自己,然后对结果执行另一个操作,你可以像这样构建一个调用链:
fac 3 3 * fac 2
fac 2 2 * fac 1
fac 1 1 * fac 0
fac 0 1
fac 1 1
fac 2 2
fac 3 6
那个深调用栈占用内存;但是,如果编译器注意到函数在进行递归调用后没有改变任何状态,则可以优化递归调用。这些类型的函数通常会传递一个 accumulator 参数。一个堆垛机朋友有一个很好的例子:Tail Recursion in Haskell
factorial 1 c = c
factorial k c = factorial (k-1) (c*k)
这个非常复杂的变化:)意味着之前的调用链变成了这样:
fac 3 1 fac 2 3
fac 2 3 fac 1 6
fac 1 6 6
(嵌套只是为了展示;运行时系统实际上不会将执行的详细信息存储在堆栈上。)
无论n 的值如何,它都在常量内存中运行,因此这种优化可以将“不可能”算法转换为“可能”算法。您会看到这种技术在函数式编程中广泛使用,就像您在 C 编程中经常看到 char * 或在 Ruby 编程中经常看到 yield 一样。
【讨论】:
当你写| condition = expression 时,它引入了一个守卫。守卫从上到下依次尝试,直到找到真正的条件,对应的表达式就是你的函数的结果。
这意味着如果n 为零,则结果为1,否则如果n > 0,则结果为fac (n-1) * n。如果n 为负数,则会出现不完整的模式匹配错误。
一旦确定了要使用的表达式,只需代入递归调用即可查看发生了什么。
fac 4
(fac 3) * 4
((fac 2) * 3) * 4
(((fac 1) * 2) * 3) * 4
((((fac 0) * 1) * 2) * 3) * 4
(((1 * 1) * 2) * 3) * 4
((1 * 2) * 3) * 4
(2 * 3) * 4
6 * 4
24
【讨论】:
特别是对于更复杂的递归案例,拯救心理健康的诀窍是不遵循递归调用,而只是假设他们“做正确的事”。例如。在您的 fac 示例中,您要计算 fac n。假设您已经拥有结果fac (n-1)。然后计算fac n 是微不足道的:只需将它乘以n。但归纳法的神奇之处在于,这种推理实际上有效(只要您提供适当的基本情况以终止递归)。所以例如对于斐波那契数,只需看看基本情况是什么,假设您能够计算所有小于 n 的数的函数:
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
看到了吗?你想计算fib n。如果您知道fib (n-1) 和fib (n-2),这很容易。但是您可以简单地假设您能够计算它们,并且递归的“更深层次”会做“正确的事情”。所以只要使用它们,它就会起作用。
请注意,编写此函数有很多更好的方法,因为目前很多值都需要重新计算。
顺便说一句:写fac 的“最佳”方式是fac n = product [1..n]。
【讨论】:
你怎么了?也许警卫(|)是令人困惑的事情。
您可以将守卫松散地认为是 if 链或 switch 语句(差异只有一个可以运行,并且它直接评估结果。不执行一系列任务,当然也没有副作用. 只是计算一个值)
平移命令式的伪代码......
Fac n:
if n == 0: return 1
if n > 0: return n * (result of calling fac w/ n decreased by one)
其他海报的调用树看起来可能会有所帮助。帮自己一个忙,真正地完成它
【讨论】: