如何编写这个Clojure函数,以便它不会烧掉堆栈?

时间:2011-12-29 17:52:11

标签: recursion clojure fibonacci

我是Clojure的新手,我认为到目前为止编写代码的方法与“Clojure方式”不一致。至少,我一直在编写使用大值导致StackOverflow错误的函数。我已经学会了使用recur,这是向前迈出的一大步。但是,如何使像下面这样的函数使用像2500000这样的值?

(defn fib [i]
  (if (>= 2 i)
    1
    (+ (fib (dec i))
       (fib (- i 2)))))

在我看来,这个函数是Fibonacci生成器的“普通”实现。我已经看到其他实现更加优化,但在他们的工作方面不那么明显。即当你阅读函数定义时,你不会去“哦,斐波那契”。

任何指针都将非常感谢!

3 个答案:

答案 0 :(得分:6)

你需要有一个关于你的功能如何工作的心智模型。假设你自己执行你的函数,每次调用使用一张纸。第一个废料,你写(fib 250000),然后你看到“哦,我需要计算(fib 249999)(fib 249998)并最终添加它们”,所以你要注意并开始两个新的废料。你不能扔掉第一个,因为当你完成其他计算时,它仍然有你要做的事情。你可以想象这个计算需要大量的资料。

另一种方法不是从顶部开始,而是从底部开始。你怎么用手工做?您将从第一个数字1,1,2,3,5,8 ...开始,然后始终添加最后两个,直到您完成i次。你甚至可以扔掉除了最后两步之外的所有数字,所以你可以重复使用大部分废料。

(defn fib [i]
  (loop [a 0
         b 1
         n 1]
    (if (>= n i)
        b
        (recur b
               (+ a b)
               (inc n)))))

这也是一个相当明显的实现,但如何,而不是 what 。当你可以简单地写下定义并且它自动转换为有效的计算时,它似乎总是很优雅,但编程就是这种转换。如果某些东西自动转换,那么这个特殊问题已经得到解决(通常以更一般的方式)。

思考“我将如何在纸上逐步完成”通常会带来良好的实施。

答案 1 :(得分:5)

以“普通”方式实现的Fibonacci生成器,如序列的定义,将始终吹嘘你的筹码。对fib的两次递归调用都不是尾递归的,这种定义无法优化。

不幸的是,如果你想编写一个适用于大数字的高效实现,你将不得不接受这样一个事实,即数学符号不会像我们希望的那样干净地转换为代码。

例如,可以在clojure.contrib.lazy-seqs中找到非递归实现。可以在Haskell wiki找到解决此问题的各种方法。理解函数式编程的基础知识应该不难理解。

答案 2 :(得分:-1)

 ;;from "The Pragmatic Programmers Programming Clojure"
 (defn fib [] (map first (iterate (fn [[a b]][b (+ a b)])[0N 1N])))
 (nth (fib) 2500000)