这是 不是 的一项家庭作业。在以下代码中:
(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)。
谢谢。
答案 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, 1
,1, 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)
这等于5
或10
。因此,您可以执行此操作,并期望它的值为15
或20
:
(+ 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)
在fib
上应用备忘录(@Sylwester的更好版本-谢谢!)
(setf (symbol-function 'fib) (memoize #'fib))
现在原来的fib
函数是记忆函数,
因此所有未成年人通话都会被记录下来。
另外,您不需要funcall
来调用已记忆的版本,
但是就做
(fib 10)