为什么基于参数的定义比递归更有效?

时间:2017-05-26 10:54:05

标签: haskell recursion

我有一个递归函数sumdown :: Int - > Int返回的总和 所有自然数从其参数下降到零,例如总和3应该返回总和3 + 2 + 1 + 0 = 6.

sumdown :: Int -> Int
sumdown 0 = 0
sumdown x = x + sumdown(x-1)

我也有这个我不完全理解的定义,有人可以为我评价一下,并告诉我为什么它可能比上面的定义更有效?

sumdown n = sumd n 0
sumd 0 a = a
sumd n a = sumd (n-1) (n+a)

感谢。

2 个答案:

答案 0 :(得分:5)

第一个递归从其结尾([0..n])开始对值n求和,如下所示:

1+(2+(3+(... + ((n-1) + n)) ...)))

通过这种方法,程序首先必须枚举所有数字,生成完整序列,然后才能实际执行添加。

这需要O(n)内存和O(n)时间。

在第二次递归中,我们正如我们之前所做的那样从0计算到n,但现在我们将数字相加,就像在

中一样
(((1+2)+3)+4)+ ...

我们可以在计算1+2之前加3。之后,我们只能保留前一个总和1+2的结果,并从内存中丢弃数字12。因此,在整个过程中,我们只保留在内存中1)到目前为止所遇到的数字之和的结果,以及2)序列中的下一个数字。

因此,我们现在只需要O(1)内存和O(n)时间。

注意:由于Haskell是惰性的,只有在每次递归时实际强制部分求和时,上述参数才成立。编译器优化器可能会默默地添加此强制,但明确说明它是一个好主意,例如:在

sumdown n = sumd n 0
sumd 0 !a = a
sumd n !a = sumd (n-1) (n+a)
-- here I am using the BangPatterns extension,
-- otherwise, seq can be used instead

第二次递归通常称为" accumulator-style",这是"尾递归的特定情况"。

(注2:尾部递归在像Haskell这样的惰性语言中并不总是一个好主意,但如果传递的数据很简单,例如数字而不是列表,尾递归通常是有益的。)

答案 1 :(得分:5)

第二个函数是尾递归,如果你遵循简化步骤,它的表现更好的原因是清晰可见的。 (虽然由于Haskell的惰性,下面的内容并不完全正确,但它给出了尾递归函数如何更有效的概念。)

sumdown 3
// 3 + sumdown 2
// 3 + (2 + sumdown 1)
// 3 + (2 + (1 + sumdown 0)
// 3 + (2 + (1 + 0))
// 3 + (2 + 1)
// 3 + 3
// 6


sumdown 3 0
// sumdown 2 3
// sumdown 1 5
// sumdown 0 6
// 6

此外,在大多数语言中,尾递归代码经过优化,可以重用相同的堆栈(因为它是递归函数的最后一个操作)。