Common Lisp中的递归,推入值和斐波那契数列

时间:2019-07-12 19:36:09

标签: recursion lisp common-lisp fibonacci clisp

这是 不是 的一项家庭作业。在以下代码中:

(defparameter nums '())

(defun fib (number)
  (if (< number 2)
      number
    (push (+ (fib (- number 1)) (fib (- number 2))) nums))
  return nums)

(format t "~a " (fib 100))

由于我对Common Lisp完全没有经验,所以我对为什么该函数不返回值感到困惑。我正在尝试打印斐波那契数列的前n个值(例如100)。

谢谢。

4 个答案:

答案 0 :(得分:6)

您的函数无条件返回nums(但仅当存在名为return的变量时)。要了解原因,我们可以像这样格式化它:

(defun fib (number)
  (if (< number 2)
    number
    (push (+ (fib (- number 1)) (fib (- number 2))) nums))
  return 
  nums)

如果number小于2,则它将无用地计算表达式number,并丢弃结果。否则,它将(+ ....)表达式的结果压入nums列表。然后,它无用地求值return,丢弃结果。如果不存在名为return的变量,那就是错误情况。否则,它将计算nums,这就是返回值。

在Common Lisp中,有一个return运算符用于终止和返回匿名命名块(名称为符号nil的块)。如果使用defun定义了一个命名函数,则将存在一个不可见的块,该块不是匿名的:它与该函数具有相同的名称。在这种情况下,可以使用return-from

(defun function ()
  (return-from function 42) ;; function terminates, returns 42
  (print 'notreached))     ;; this never executes

某些标准控制流和循环结构建立了一个隐藏的匿名块,因此可以使用return

 (dolist (x '(1 2 3))
   (return 42))            ;; loop terminates, yields 42 as its result

如果我们使用(return ...),但没有封闭的匿名块,那就是错误。

表达式(return ...)return不同,表达式return评估由符号fib命名的变量,并检索其内容。

由于要求未知,因此尚不清楚如何修复{{1}}函数。将值推入全局列表的副作用通常不属于这样的数学函数,它应该是纯函数(无副作用)。

答案 1 :(得分:6)

一种计算斐波纳契数的明显方法是:

(defun fib (n)
  (if (< n 2)
      n
    (+ (fib (- n 1)) (fib (- n 2)))))

(defun fibs (n)
  (loop for i from 1 below n
        collect (fib i)))

请稍加思考,为什么没有这样的方法可以帮助您计算前100个斐波纳契数:计算(fib n)所花费的时间等于或多于计算{ {1}}加上计算(fib (- n 1))所花费的时间:这是指数的(请参阅this stack overflow answer)。

一个很好的解决方案是 memoization (fib (- n 2))的计算会重复进行多次子计算,如果我们只记得我们上次计算的答案,就可以避免这样做再次如此。

(此答案的较早版本此处具有一个过于复杂的宏:类似的内容通常可能有用,但此处不需要。)

您可以在这里记住(fib n)

fib

这将保存到目前为止已计算出的结果的表(列表),并使用它来避免重新计算。

使用该功能的此备忘版本:

(defun fib (n)
  (check-type n (integer 0) "natural number")
  (let ((so-far '((2 . 1) (1 . 1) (0 . 0))))
    (labels ((fibber (m)
               (when (> m (car (first so-far)))
                 (push (cons m (+ (fibber (- m 1))
                                  (fibber (- m 2))))
                       so-far))
               (cdr (assoc m so-far))))
      (fibber n))))

上面的定义为每次对> (time (fib 1000)) Timing the evaluation of (fib 1000) User time = 0.000 System time = 0.000 Elapsed time = 0.000 Allocation = 101944 bytes 0 Page faults 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875 的调用使用了一个新的缓存:这很好,因为本地函数fib确实重用了缓存。但是您可以通过将缓存完全放在函数之外来做得更好:

fibber

此版本的(defmacro define-function (name expression) ;; Install EXPRESSION as the function value of NAME, returning NAME ;; This is just to avoid having to say `(setf ...)`: it should ;; probably do something at compile-time too so the compiler knows ;; the function will be defined. `(progn (setf (fdefinition ',name) ,expression) ',name)) (define-function fib (let ((so-far '((2 . 1) (1 . 1) (0 . 0)))) (lambda (n) (block fib (check-type n (integer 0) "natural number") (labels ((fibber (m) (when (> m (car (first so-far))) (push (cons m (+ (fibber (- m 1)) (fibber (- m 2)))) so-far)) (cdr (assoc m so-far)))) (fibber n)))))) 将在调用之间共享其高速缓存,这意味着速度更快,分配的内存更少,但线程安全性可能更低:

fib

有趣的是,回忆录是由唐纳德·米奇(Donald Michie)发明的(或者至少是他的名字),他曾尝试打破Tunny(并因此与Colossus并肩作战),而我也略有了解:计算的历史还很短!


请注意,记忆化是您最终可能无法与编译器抗争的时候之一。特别是对于这样的功能:

> (time (fib 1000))
[...]
Allocation   = 96072 bytes
[...]

> (time (fib 1000))
[...]
Allocation   = 0 bytes
[...]

然后允许(但不是必需)编译器假定对(defun f (...) ... ;; no function bindings or notinline declarations of F here ... (f ...) ...) 的明显递归调用是对其正在编译的函数的递归调用,从而避免了整个函数的大量开销呼叫。特别是,不需要检索符号f的当前函数值:它可以直接直接调用函数本身。

这意味着试图编写 function f的尝试,因为memoize可能无法使用,从而无法使用现有的递归函数:该函数(setf (fdefinition 'f) (memoize #'f))仍直接调用其自身的未存储版本,不会注意到f的函数值已更改。

实际上,即使递归是间接的,也是如此:允许编译器假定对在同一文件中有定义的函数f的调用是对已定义版本的调用在文件中,并再次避免进行完整调用的开销。

处理此问题的方法是添加适当的g声明:如果调用被notinline声明覆盖(编译器必须知道),则必须将其完整呼叫。来自spec

  

编译器不能随意忽略此声明。对指定函数的调用必须实现为离线子例程调用。

这意味着,为了记忆功能,您必须为递归调用添加合适的notinline声明,这意味着记忆需要要么由宏完成,要么必须依赖于用户添加对要记忆的功能的适当声明。

这只是一个问题,因为允许CL编译器很聪明:几乎总是一件的事情!

答案 2 :(得分:3)

因此,您可能知道,如果您知道前两个数字,则可以计算下一个。 3, 5之后会怎样?如果您猜到8,您已经理解了。现在,如果您从0, 1开始并滚动1, 11, 2等,您将收集第一个变量,直到获得所需数量的数字为止:

(defun fibs (elements)
  "makes a list of elements fibonacci numbers starting with the first"
  (loop :for a := 0 :then b
        :for b := 1 :then c
        :for c := (+ a b)
        :for n :below elements
        :collect a))

(fibs 10)
; ==> (0 1 1 2 3 5 8 13 21 34)

Common Lisp中的每种形式都会“返回”一个值。您可以说它评估为。例如。

(if (< a b)
    5
    10)

这等于510。因此,您可以执行此操作,并期望它的值为1520

(+ 10 
   (if (< a b)
       5
       10))

您基本上希望您的函数具有一个表达式来计算结果。例如。

(defun fib (n)
  (if (zerop n)
      n
      (+ (fib (1- n)) (fib (- n 2)))))

这将计算if表达式的结果... loop:collect一起返回列表。您也有(return expression)(return-from name expression),但通常是不必要的。

答案 3 :(得分:1)

您的全局变量num实际上并不是一个坏主意。

它将有一个中央存储器,已计算出斐波那契数。而且不要再次计算那些已经计算出的数字。

这就是记忆的主意。

但是首先,我对全局变量的处理方式很糟糕。

具有全局变量*fibonacci*的错误版本

(defparameter *fibonacci* '(1 1))

(defun fib (number)
  (let ((len (length *fibonacci*)))
    (if (> len number)
        (elt *fibonacci* (- len number 1)) ;; already in *fibonacci*
        (labels ((add-fibs (n-times)
                   (push (+ (car *fibonacci*)
                            (cadr *fibonacci*))
                         *fibonacci*)
                   (cond ((zerop n-times) (car *fibonacci*))
                         (t (add-fibs (1- n-times))))))
          (add-fibs (- number len))))))

;;> (fib 10)
;;  89
;;> *fibonacci*
;;  (89 55 34 21 13 8 5 3 2 1 1)

良好的功能版本(记忆)

在备忘录中,您隐藏了全局*fibonacci*变量 进入词汇功能(功能的记忆版本)的环境。

(defun memoize (fn)
  (let ((cache (make-hash-table :test #'equal)))
    #'(lambda (&rest args)
        (multiple-value-bind (val win) (gethash args cache)
          (if win
              val
              (setf (gethash args cache)
                    (apply fn args)))))))

(defun fib (num)
  (cond ((zerop num) 1)
        ((= 1 num) 1)
        (t (+ (fib (- num 1))
              (fib (- num 2))))))

以前的全局变量*fibonacci*实际上是cache函数的局部变量memoize-封装/隐藏在全局环境中, 只能通过功能fibm进行访问/查找。

fib上应用备忘录(错误版本!)

(defparameter fibm (memoize #'fib))

由于通用Lisp是Lisp 2(函数名和变量名之间的分隔名称空间),但是我们在这里必须将备注函数分配给变量, 我们必须使用(funcall <variable-name-bearing-function> <args for memoized function>)

(funcall fibm 10) ;; 89

或者我们定义一个额外的

(defun fibm (num)
  (funcall fibm num))

并且可以做到

(fibm 10)
  • 但是,这仅保存/存储外拨电话,例如这里只有 斐波那契数值为10。尽管如此,斐波那契数字 也计算9、8,...,1。 要保存它们,请看下一节!

fib上应用备忘录(@Sylwester的更好版本-谢谢!)

(setf (symbol-function 'fib) (memoize #'fib))

现在原来的fib函数是记忆函数, 因此所有未成年人通话都会被记录下来。 另外,您不需要funcall来调用已记忆的版本, 但是就做

(fib 10)