Haskell:改进我的尾部递归fibonacci实现

时间:2012-07-25 18:45:47

标签: haskell fibonacci tail-recursion

我已经提出了以下尾递归斐波纳契生成器:

let {
  fibo :: Integral x => [x]->x->x->x->[x]
  fibo l x y 0 = l
  fibo l x y n = fibo (l ++ [y+x] ++ [y+x+y]) (x+y) (y+x+y) (n-1)
}

请原谅我把整个实现放在一行,因为我使用GHCi并且还没有完全学会如何把它放在一个文件中并运行(我还没到达那里)。我想知道的是这个电话:

fibo [0, 1] 0 1 5

可以改进。我不想用0和1传递初始列表,然后再次使用限制传递0和1。我相信可以改变实施。可以做些什么改变?

3 个答案:

答案 0 :(得分:7)

你的算法是尾递归的,但看起来它有其它缺点,即1)你通过追加到它的末尾来构建结果列表,2)它不是懒惰的。

对于1),请注意,当您附加两个列表a ++ b时,您基本上必须重新分配a。在您的情况下,a是斐波纳契数字的增长列表,b是接下来的两个术语。因此,每次迭代都会重新分配已经计算过的斐波纳契数,并添加两个元素,这将导致二次运行时间。将b添加到a的前面会更有效率。你将反过来生成斐波纳契数,但运行时间是线性的。如果您希望序列按升序排列,则最后可以reverse列表。

请注意,以相反的顺序构建列表可以让您使用Code-Guru的模式匹配理念轻松获取序列的最后两个术语。

对于2),请注意,在整个计算完成之前,您无法获取列表的第一个元素。与下面的延迟生成序列进行比较:

fibs = 0 : (go 0 1)
  where go a b = b : go b (a+b)

即使递归似乎永远不会停止,fibs只会根据需要进行评估。例如,fibs !! 3只会多次调用go

答案 1 :(得分:4)

我不会去算法本身,但是这里有一些关于如何构造递归函数的建议。

首先,以下是如何在单独的文件中格式化代码

fibo :: Integral x => [x]->x->x->x->[x]
fibo l x y 0 = l
fibo l x y n = fibo (l ++ [y+x] ++ [y+x+y]) (x+y) (y+x+y) (n-1)

如果将其保存为fibo.hs,则可以使用

启动GHCi
ghci fibo.hs

在开始时加载文件。您也可以使用命令

启动GHCi后加载文件
:l fibo.hs

(假设您在保存fibo.hs的同一目录中启动GHCi)

另一个不错的功能是,现在编辑文件时,只需输入

即可重新加载所有更改
:r

在GHCi提示符中。

现在,为了摆脱额外的参数,Haskell中的通常模式是将递归部分重构为其自己的辅助函数,并将主函数作为仅接受必要参数的入口点。例如,

fibo :: Integral x => x -> [x]
fibo n = fiboHelper [0,1] 0 1 n

fiboHelper :: Integral x => [x]->x->x->x->[x]
fiboHelper l x y 0 = l
fiboHelper l x y n = fiboHelper (l ++ [y+x] ++ [y+x+y]) (x+y) (y+x+y) (n-1)

现在,您只需使用

即可致电fibo
> fibo 3
[0,1,1,2,3,5,8,13]

此外,这样一个无用的辅助函数通常使用letwhere作为内部函数隐藏在main函数中。

fibo :: Integral x => x -> [x]
fibo n = fiboHelper [0,1] 0 1 n where
    fiboHelper l x y 0 = l
    fiboHelper l x y n = fiboHelper (l ++ [y+x] ++ [y+x+y]) (x+y) (y+x+y) (n-1)

这样的内部函数通常被赋予较短的名称,因为上下文明确了其目的,因此我们可以将名称更改为例如fibo'

go是递归辅助函数的另一个常用名称。

答案 2 :(得分:3)

仅供记录:Fibonacci数字列表的“通常”定义是:

fibo = 0 : scanl (+) 1 fibo