【问题标题】:The lisp-way to solve Fibonnaci解决斐波那契的 lisp 方法
【发布时间】:2010-10-12 06:14:16
【问题描述】:

我想尝试学习 Lisp,但很快就放弃了。我想我会再试一次。我正在查看 Problem 2 on Project Euler - 查找所有低于 400 万的偶数斐波那契数的总和。

我编写了以下代码,它可以工作,但很丑陋。其中最主要的是它非常慢 - 因为它一直在进行天真的递归。

当我用 Python 编写这个程序时,我在计算时建立了一个列表,从不重新计算数字。我知道我可以在这里(以某种方式)做到这一点,但这似乎不符合 lisp 和函数式编程的精神。在 #3 之后我放弃了,当我达到递归深度限制并且不得不重写我的代码以使用循环而不是递归。

所以我想我的问题是:

  1. 解决此问题的“正确、简洁的方法”是什么?
  2. 您如何协调递归和“仅计算所有内容”的概念与计算所有内容的实际限制?
  3. 除了 The Little Schemer 和 Project Euler 之外,还有什么关于学习 lisp 的建议吗?

这是我的代码:

 (defun fib(i)
   (if (= i 1)                   ;Could rewrite this as a case statement
     1
     (if (= i 2)
       1
       (+ (fib (- i 1)) (fib (- i 2))))))

 (defun solve(i)
   (let ((f (fib i)))            ;Store result in local variable
     (print f)                   ;For debugging
     (if (< 4000000 f)
       0                         ;return
       (if (= 0 (mod f 2))
         (+ f (solve (+ i 1)))   ;add number
         (solve (+ i 1))))))     ;don't

 (print (solve 1))

【问题讨论】:

  • 请,在编写 lisp 代码时不要乱扔括号。请参阅gigamonkeys.com/book/syntax-and-semantics.html 的最后一段“格式化 Lisp 代码”以获取指南。
  • 我部分不同意。 “Lonely-parens” 是不好的风格,但如果您使用它们的原因是为了帮助习惯使用 Lisp 编程,那么您将获得更多的力量。只要记住最终要塑造自己的风格。
  • @Tom Ritter 为什么不使用 paredit 模式。它摇摆不定。试一试:)

标签: lisp common-lisp fibonacci


【解决方案1】:

这是一个记忆版本。 在这种情况下,由于您必须计算每个斐波那契数,直到找到超过 400 万的一个,所以使用封闭形式的解决方案没有多大用处。

这种方法通过let创建了一个词法环境;创建字典fib-table 以及该环境中的函数fib-memoed。最终产品是 fib-table 可以从 fib-memoed 访问,但没有其他地方。

现在是踢球者:因为我们必须计算 fib 的顺序值直到满足某些条件,所以我们在 fib 1 处启动求解器。从这里开始,计算下一个 fib number 最多需要 2 次字典查找和 1 次加法:O(1) BOOM!

;; memoised fib function: make a hash table, then create                                      
;; the fib function in the lexical scope of the hash table                                    
(let ((fib-table (make-hash-table)))
  (setf (gethash 0 fib-table) 1)
  (setf (gethash 1 fib-table) 1)
  (defun fib-memoed (n)
    (let ((x (gethash n fib-table)))
      (cond ((not (null x))
             x)
            (t
             (let ((y (+ (fib-memoed (- n 1))
                         (fib-memoed (- n 2)))))
               (setf (gethash n fib-table) y)
               y))))))

(defun solve ()
  (let ((maxval 4000000))
    (labels ((aux (i acc)
               (let ((x (fib-memoed i)))
                 (cond ((> x maxval) acc)
                       ((evenp x)
                        (aux (1+ i) (+ x acc)))
                       (t (aux (1+ i) acc))))))
      (aux 1 0))))

【讨论】:

    【解决方案2】:

    ;;;我尝试按照@PESTO 的建议编写代码

    (defun Fibo-Test (n / one_prior two_prior curr sum i)

    (setq i 2) (setq one_prior 1 two_prior 1 )

    (条件

    ((= n 0) (setq 总和 0) )

    ((= n 1) (setq 总和 1) )

    ((= n 2) (setq 总和 1) )

    (T

    (while (and (

    ;;;转换为实数

    (setq sum (+ (float one_prior) (float two_prior)))

    (setq i (1+ i))

    (setq 当前总和) (setq
    one_prior two_prior two_prior 当前
    )

    ) ;结束时

    ) ;结束条件(T)

    ) ;结束条件

    (princ (strcat "\nF(" (itoa i) ") = " (RTOS sum) " . "))

    (原则)

    ) ;结束函数 Fibo-Test

    【讨论】:

    • 这是一个答案还是你有什么问题,这是一个问题?
    【解决方案3】:

    创建斐波那契数列的简单有效的方法:

    (defun fibs (n &optional (a 1) (b 1))
      (loop repeat n 
            collect (shiftf a b (+ a b))))

    (shiftf) 可以取任意数量的位置,最后取一个值。每个位置都设置为下一个变量的值,最后一个变量取其后的值。它返回第一个位置的值。换句话说,它将所有值左移一。

    但是,您不需要完整列表(您只需要偶数)并且您根本不需要列表(您只需要总和),因此可以直接将其用于函数。每三个斐波那契数都是偶数,所以...

    (defun euler-2 (limit &optional (a 1) (b 1))
      (loop for x upfrom 1
            until (> a limit)
            if (zerop (mod x 3)) 
               sum a
            do (shiftf a b (+ a b))))

    【讨论】:

      【解决方案4】:

      除了所有有用的答案之外,以下公式可能会提供更高的效率 - 以 O(Log(n)) 而不是 O(2^n) 计算 Fn。这必须与记忆相结合,是解决问题的坚实基础:

      F(2*n) = F(n)^2 + F(n-1)^2
      
      F(2*n + 1) = ( 2*F(n-1) + F(n) )   *   F(n)
      

      【讨论】:

      【解决方案5】:
      (defun fib (x &optional (y 0) (z 1))
                 (if (< x z)
                     nil
                     (append (list z) (fib x z (+ y z)))))
      
      CL-USER> (reduce #'+ (remove-if-not #'evenp (fib 1000000)))
      

      【讨论】:

      • 不幸的是,这通常不是一个好主意,因为像这样的递归函数可能会造成堆栈溢出(取决于最大堆栈大小和递归深度)。方案一会将其重写为尾递归样式。在 Common Lisp 中,实际上我们需要使用迭代构造。
      • (append (list 1) (foo ...)) = (cons 1 (foo ...))
      • 两种“解决方案”都是错误的。您没有正确阅读问题。
      • @leppie 你为什么不尝试回答这个问题,而不是成为无用的评论员并投票否决其他人。有点道德,回答问题,伙计。
      【解决方案6】:

      http://fare.tunes.org/files/fun/fibonacci.lisp 有一个解决斐波那契的演练,逐步提高实现的时间和内存性能。

      【讨论】:

        【解决方案7】:

        使用tail recursion 而不是简单的递归。大多数 Lisp 实现应该执行尾调用优化;不再有递归深度限制。

        除此之外,试着从列表和可以对这些列表执行的抽象操作的角度来思考问题。需要考虑的两个更相关的操作:

        关于其他 Lisp 资源:

        更新: 尾递归方案fib函数:

        (define (fib n)
          (fib-tr n 1 0))
        
        (define (fib-tr n next result)
          (cond ((= n 0) result)
                (else (fib-tr (- n 1) (+ next result) next))))
        

        【讨论】:

        • 但是尾调用并不能解决 O(2^n) 问题。您仍然一遍又一遍地重新计算相同的值。
        • 向尾递归的转换通常会导致迭代过程,即使它在技术上使用递归。
        • @Hank,我认为在这种情况下不会。它会阻止堆栈增长,但您最终仍会一遍又一遍地计算 fib(n)。
        • 这是我最常看到的尾递归实现,它通过保持运行计数来工作,因此它不是指数大 O。
        • 为什么要有单独的功能? fib-tr 可以是 let 表达式: (let fib-tr ((a 1) (b 1) (cnt 10)) (display a) (newline) (if (> cnt 0) (fib-tr b (+ ab ) (-cnt 1)) #f))
        【解决方案8】:
        (let ((a 1) (b 1))
          (flet ((nextfib ()
                   (prog1 a
                     (psetf a b b (+ a b)))))
            (loop for fib = (nextfib)
                  while (<= fib 4000000)
                  when (evenp fib)
                    sum fib)))
        

        上面定义了一个函数 NEXTFIB,它将为每个调用生成下一个斐波那契数。 LOOP 将偶数结果相加,上限为 4000000。

        PROG1 返回其第一个子表达式的值。 PSETF 将 a 和 b 设置为“平行”。

        这是一种常见的模式。有一个生成器函数,可以重复调用它,过滤结果并组合它们。

        【讨论】:

          【解决方案9】:

          danio 的回答对性能问题有很大帮助。

          以下是对您风格的简短评论:

           (defun fib(i)
             (if (= i 1) ;//Could rewrite this as a case statement
               1
               (if (= i 2)
                 1
                 (+ (fib (- i 1)) (fib (- i 2)))
               )
             )
           )
          

          将嵌套的 IF 重构为 COND。

          不要将括号单独放在一行中。

          (defun 解决(i) (let ((f (fib i))) ;//将结果存入局部变量 (print f) ;//用于调试 (如果 (

          使用 ZEROP 更清晰。

          (+ f (solve (+ i 1))) ;//加数 (solve (+ i 1)) ;//不要

          你为什么把那些 // 放进去?分号后跟一个空格就足够了。

          ) ) ) ) (打印(解决1))

          你最后的 PRINT 声明让我有点怀疑。你是从文件还是从 REPL 运行这个程序?如果你做前者,那么你应该考虑做后者。如果你做后者,你可以说 (solve 1) 得到结果。

          【讨论】:

          • 我把 // 仅用于降价自动格式化。它不识别语言,并在 cmets 中突出显示关键字。但它确实识别 // 并将其后的所有内容视为注释。
          【解决方案10】:

          我对“lisp 精神”的理解是将自己从任何固定的、教条主义的、固执己见的 lisp 精神观念中分离出来,并使用最能反映解决问题所需的计算结构的 lisp 结构。例如,

          (defun euler2 (&optional (n 4000000))
            (do ((i 1 j)
                 (j 2 (+ i j))
                 (k 0))
                ((<= n i) k)
              (when (evenp i) (incf k i))))
          

          如果你坚持递归,这里有另一种方式:

          (defun euler2 (&optional (n 4000000))
            (labels ((fn (i j k) 
                       (if (<= n i) k (fn j (+ i j) (if (oddp i) k (+ k i))))))
              (fn 1 2 0)))
          

          【讨论】:

            【解决方案11】:

            为了扩展 Danio 的回答,http://fare.tunes.org/files/fun/fibonacci.lisp 上的文章介绍了两种使代码运行得更快的方法。使用直接递归(尾调用或否)是 O(2^n) 并且非常慢。困难在于每个值都被一遍又一遍地计算。你必须以不同的方式做事。这两个建议是:

            1. 使用迭代方法。
            (defun bubble-fib (n)
              (declare (optimize (speed 3) (safety 0) (debug 0)))
              (check-type n fixnum)
              (loop repeat n
              with p = 0 with q = 1
              do (psetq p q
                    q (+ p q))
              finally (return p)))
            
            1. 使用记忆。这意味着记住之前看到的值并重新调用它们,而不是重新计算它们。这篇文章提供了一个 CL 包,可以执行此操作以及一些代码供您自己执行。

            【讨论】:

              【解决方案12】:

              Memoization 是一种将结果缓存到函数的方法,以避免一遍又一遍地重新计算中间结果。记忆化基本上意味着你第一次用一些参数调用一个函数,计算答案并返回它,然后缓存那个答案;对于后续调用具有相同参数的函数,只需返回缓存值。

              在 Lisp 中,您可以轻松地使用高阶函数和宏来透明地记忆函数。 Clojure 将memoize 作为包含的标准函数。另请参阅 On Lisp 的第 65 页,了解 memoize 的 Common Lisp 实现。这是在 Clojure 中:

              (defn fib-naive [i]
                (if (or (= i 1) (= i 2))
                  1
                  (+ (fib-naive (- i 1)) (fib-naive (- i 2)))))
              
              (def fib-memo
                   (memoize (fn [i]
                              (if (or (= i 1) (= i 2))
                                1
                                (+ (fib-memo (- i 1)) (fib-memo (- i 2)))))))
              
              user> (time (fib-naive 30))
              "Elapsed time: 455.857987 msecs"
              832040
              user> (time (fib-memo 30))
              "Elapsed time: 0.415264 msecs"
              832040
              user> 
              

              如果你在一个大整数上调用它,这仍然会导致堆栈溢出。例如立即执行(fib 10000) 会破坏堆栈,因为它仍然必须非常深入地递归(一次)。但是,如果您先启动缓存,则不再需要深度递归,这可以避免。只需先执行此操作(在 Clojure 中):

              (dorun (map fib-memo (range 1 10000)))
              

              足以让你毫无问题地做(fib 10000)

              (计算斐波那契数的具体主题最近出现在 Clojure mailing list 上。那里有一个基于 Lucas numbers 的解决方案,我一点也不明白,但据说比天真的快 40 倍算法。)

              【讨论】:

                【解决方案13】:

                【讨论】:

                  【解决方案14】:

                  解决这个问题的方法是自下而上地工作,一个接一个地生成每个斐波那契项,如果它是偶数,则将其添加到总和中,并在达到 400 万阈值时停止。我的 LISP 生锈了,所以这里是伪代码:

                  one_prior = 1
                  two_prior = 1
                  curr = 2
                  sum = 0
                  while curr < 4000000000
                    if curr % 2 == 0
                      sum = sum + curr
                    two_prior = one_prior
                    one_prior = curr
                    curr = one_prior + two_prior
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2019-05-10
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2015-01-06
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多