【问题标题】:Haskell: Improving my tail-recursive fibonacci implementationHaskell:改进我的尾递归斐波那契实现
【发布时间】:2012-07-24 06:49:54
【问题描述】:

我想出了以下可行的尾递归斐波那契生成器:

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。我相信实施是可以改变的。可以做哪些改变?

【问题讨论】:

  • 您可以像在解释器中那样将其逐行写入文件中(也无需将所有内容都包装在 let 块中),然后在解释器中加载文件(在 GHCi 中)带 :l 文件名)
  • 你的代码是尾递归的,但是效率很低,因为(++) 在它的第一个参数中是线性的。但我想这不是问题的一部分。
  • Haskell 中的尾递归不需要像在严格语言中那样持续使用堆栈,相反,非尾递归也不需要线性堆栈使用,所以我质疑你练习的价值。坦率地说,我会选择经典的 fibs = 0 : 1 : zipWith (+) fibs (tail fibs) 或其变体之一 scanlunfoldr。请参阅this HaskellWiki page 了解一系列实现。

标签: haskell fibonacci tail-recursion


【解决方案1】:

您的算法是尾递归的,但它看起来还有其他缺点,即 1)您通过附加到结果列表的末尾来构建结果列表,以及 2)它不是惰性的。

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

请注意,通过使用 Code-Guru 的模式匹配思想,以相反的顺序构建列表可以让您轻松获得序列的最后两项。

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

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

即使看起来递归永远不会停止,fibs 只会根据需要进行评估。例如,fibs !! 3 只调用了几次go

【讨论】:

  • ...这意味着我可以从 Haskell 的函数外部控制递归?
  • 是的 - 这是一种看待它的方式 - 您不必提前决定要计算多少项
【解决方案2】:

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

首先,您将如何在单独的文件中格式化您的代码

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 中的常用模式是将递归部分重构为它自己的辅助函数,并将 main 函数作为只接受必要参数的入口点。例如,

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 作为内部函数隐藏在主函数中。

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 是递归辅助函数的另一个常用名称。

【讨论】:

    【解决方案3】:

    仅作记录:斐波那契数列的“通常”定义是:

    fibo = 0 : scanl (+) 1 fibo
    

    【讨论】:

    • 或者(稍微不那么神秘)fibo = 0 : 1 : zipWith (+) fibo (tail fibo)
    猜你喜欢
    • 2021-11-27
    • 2014-04-02
    • 2018-02-17
    • 1970-01-01
    • 2010-12-03
    • 2011-12-12
    • 2011-09-27
    • 1970-01-01
    相关资源
    最近更新 更多