为什么我的迭代高阶过程比等效的递归过程给出更精确的结果?
Why does my iterative higher-order procedure give more precise results than my equivalent recursive procedure?
Exercise 1.30 of SICP 邀请我们将以下代码重写为迭代(阅读:尾递归)过程:
(define (sum term a next b)
(if (> a b)
0
(+ (term a)
(sum term (next a) next b))))
为了帮助我们,我们得到了以下模板:
(define (sum term a next b)
(define (iter a result)
(if <??>
<??>
(iter <??> <??>)))
(iter <??> <??>))
在一次错误的开始后,我给出了以下答案,看起来是正确的:
(define (sumIT term a next b)
(define (iter a result)
(if (> a b)
result
(iter (next a) (+ (term a) result))))
(iter a 0))
为了对此进行测试,我从给出此练习的正上方复制了以下代码:
(define (integral f a b dx)
(define (add-dx x) (+ x dx))
(* (sum f (+ a (/ dx 2.0)) add-dx b)
dx))
(integral cube 0 1 0.01)
(integral cube 0 1 0.001)
并使用 sumIT
:
快速制作了这个版本
(define (integralIT f a b dx)
(define (add-dx x) (+ x dx))
(* (sumIT f (+ a (/ dx 2.0)) add-dx b)
dx))
(integralIT cube 0 1 0.01)
(integralIT cube 0 1 0.001)
然后,由于我运行在DrRacket的#lang sicp模式下,默认情况下没有cube
过程,所以我还定义了:
(define (cube x) (* x x x))
最后,我运行代码。
不出所料,(integral cube 0 1 0.01)
和 (integralIT cube 0 1 0.01)
都产生完全相同的结果:0.24998750000000042。但是,令我震惊和恐惧的是,(integral cube 0 1 0.001)
returns 0.249999875000001 而 (integralIT cube 0 1 0.001)
returns 更精确的答案是 0.24999987500000073.
这是为什么?为什么将递归高阶过程重写为尾递归会提高结果的精度?我想不出我的代码中会导致这种情况的任何部分或错误。
浮点加法和乘法是可交换的,但不是结合的。这意味着由于错误传播,(+ (+ a b) c)
可能不等于 (+ a (+ b c)
。正如 Óscar López 已经提到的,请参阅 What Every Computer Scientist Should Know About Floating-Point Arithmetic
.
我用 Common Lisp 重写了您的示例,我可以观察到相同的行为。
然后我通过插入反引号和逗号稍微更改了代码,这样函数 returns 就变成了一棵树而不是一个数字。如果您不熟悉语法,请参阅 Macros: defining your own。
树由数字和符号(*
、+
、...)组成,对应于每个版本中所写的操作。
这应该有助于理解如何以及何时计算中间结果。
递归求和:
(defun sum (term a next b)
(labels ((it (a)
(if (> a b)
0
`(+ ,(it (funcall next a))
,(funcall term a)))))
(it a)))
尾递归和:
(defun sum-it (term a next b)
(labels ((it (a result)
(if (> a b)
result
(it (funcall next a)
`(+ ,(funcall term a) ,result)))))
(it a 0)))
递归积分:
(defun integral (f a b dx)
(flet ((add-dx (x) (+ x dx)))
`(* ,(sum f (+ a (/ dx 2.0d0)) #'add-dx b) ,dx)))
尾递归积分:
(defun integral-it (f a b dx)
(flet ((add-dx (x) (+ x dx)))
`(* ,(sum-it f (+ a (/ dx 2.0d0)) #'add-dx b) ,dx)))
和立方体:
(defun cube (x) (* x x x))
测试时要小心:目标精度参数过于精确,结果会很大。
您可以看到这两种方法最终以不同的顺序计算结果:
CL-USER> (integral #'cube 0 1 0.4d0)
(* (+ (+ (+ 0 1.0d0) 0.21600000000000008d0) 0.008000000000000002d0) 0.4d0)
相比于:
CL-USER> (integral-it #'cube 0 1 0.4d0)
(* (+ 1.0d0 (+ 0.21600000000000008d0 (+ 0.008000000000000002d0 0))) 0.4d0)
在上面的例子中,结果并没有不同,但是对于像你找到的那样的一些输入,会有不同。
Exercise 1.30 of SICP 邀请我们将以下代码重写为迭代(阅读:尾递归)过程:
(define (sum term a next b)
(if (> a b)
0
(+ (term a)
(sum term (next a) next b))))
为了帮助我们,我们得到了以下模板:
(define (sum term a next b)
(define (iter a result)
(if <??>
<??>
(iter <??> <??>)))
(iter <??> <??>))
在一次错误的开始后,我给出了以下答案,看起来是正确的:
(define (sumIT term a next b)
(define (iter a result)
(if (> a b)
result
(iter (next a) (+ (term a) result))))
(iter a 0))
为了对此进行测试,我从给出此练习的正上方复制了以下代码:
(define (integral f a b dx)
(define (add-dx x) (+ x dx))
(* (sum f (+ a (/ dx 2.0)) add-dx b)
dx))
(integral cube 0 1 0.01)
(integral cube 0 1 0.001)
并使用 sumIT
:
(define (integralIT f a b dx)
(define (add-dx x) (+ x dx))
(* (sumIT f (+ a (/ dx 2.0)) add-dx b)
dx))
(integralIT cube 0 1 0.01)
(integralIT cube 0 1 0.001)
然后,由于我运行在DrRacket的#lang sicp模式下,默认情况下没有cube
过程,所以我还定义了:
(define (cube x) (* x x x))
最后,我运行代码。
不出所料,(integral cube 0 1 0.01)
和 (integralIT cube 0 1 0.01)
都产生完全相同的结果:0.24998750000000042。但是,令我震惊和恐惧的是,(integral cube 0 1 0.001)
returns 0.249999875000001 而 (integralIT cube 0 1 0.001)
returns 更精确的答案是 0.24999987500000073.
这是为什么?为什么将递归高阶过程重写为尾递归会提高结果的精度?我想不出我的代码中会导致这种情况的任何部分或错误。
浮点加法和乘法是可交换的,但不是结合的。这意味着由于错误传播,(+ (+ a b) c)
可能不等于 (+ a (+ b c)
。正如 Óscar López 已经提到的,请参阅 What Every Computer Scientist Should Know About Floating-Point Arithmetic
.
我用 Common Lisp 重写了您的示例,我可以观察到相同的行为。
然后我通过插入反引号和逗号稍微更改了代码,这样函数 returns 就变成了一棵树而不是一个数字。如果您不熟悉语法,请参阅 Macros: defining your own。
树由数字和符号(*
、+
、...)组成,对应于每个版本中所写的操作。
这应该有助于理解如何以及何时计算中间结果。
递归求和:
(defun sum (term a next b)
(labels ((it (a)
(if (> a b)
0
`(+ ,(it (funcall next a))
,(funcall term a)))))
(it a)))
尾递归和:
(defun sum-it (term a next b)
(labels ((it (a result)
(if (> a b)
result
(it (funcall next a)
`(+ ,(funcall term a) ,result)))))
(it a 0)))
递归积分:
(defun integral (f a b dx)
(flet ((add-dx (x) (+ x dx)))
`(* ,(sum f (+ a (/ dx 2.0d0)) #'add-dx b) ,dx)))
尾递归积分:
(defun integral-it (f a b dx)
(flet ((add-dx (x) (+ x dx)))
`(* ,(sum-it f (+ a (/ dx 2.0d0)) #'add-dx b) ,dx)))
和立方体:
(defun cube (x) (* x x x))
测试时要小心:目标精度参数过于精确,结果会很大。
您可以看到这两种方法最终以不同的顺序计算结果:
CL-USER> (integral #'cube 0 1 0.4d0)
(* (+ (+ (+ 0 1.0d0) 0.21600000000000008d0) 0.008000000000000002d0) 0.4d0)
相比于:
CL-USER> (integral-it #'cube 0 1 0.4d0)
(* (+ 1.0d0 (+ 0.21600000000000008d0 (+ 0.008000000000000002d0 0))) 0.4d0)
在上面的例子中,结果并没有不同,但是对于像你找到的那样的一些输入,会有不同。