我试图实现这样定义的函数:
f(n) = n if n < 4
f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3) + 4f(n - 4) if n >= 4
执行此操作的迭代方法是从底部开始直到我点击n
,所以如果n = 6:
f(4) = (3) + 2(2) + 3(1) + 4(0) | 10
f(5) = f(4) + 2(3) + 3(2) + 4(1) | 10 + 16 = 26
f(6) = f(5) + 2f(4) + 3(3) + 4(2) | 26 + 2(10) + 17 = 63
实施尝试:
; m1...m4 | The results of the previous calculations (eg. f(n-1), f(n-2), etc.)
; result | The result thus far
; counter | The current iteration of the loop--starts at 4 and ends at n
(define (fourf-iter n)
(cond [(< n 4) n]
[else
(define (helper m1 m2 m3 m4 result counter)
(cond [(= counter n) result]
[(helper result m1 m2 m3 (+ result m1 (* 2 m2) (* 3 m3) (* 4 m4)) (+ counter 1))]))
(helper 3 2 1 0 10 4)]))
几个问题:
f(4) = 10
result
,在3开始counter
,以便将计算应用于m1
到m4
(以及f(4)
实际上是计算出来而不是预设的),但是0
在下一次迭代中被用作m1,而它应该是f(4)
的结果(10
)tl; dr结果计算延迟,或结果本身延迟。我怎样才能正确写出来?
答案 0 :(得分:1)
我认为编写一个递归定义的函数的相应“Scheme-ish”方法就是使用 memoization 。如果函数f
是 memoized ,那么当您首先调用f(4)
时,它会在键值表中查找4
,如果找到它,则返回储值。否则,它只是正常计算,然后存储它在表中计算的任何内容。因此,f
永远不会评估相同的计算两次。这类似于制作大小为n
的数组并填充从0开始的值的模式,为n
构建解决方案。该方法称为动态编程,而memoization和动态编程实际上是查看相同优化策略的不同方式 - 避免两次计算相同的事情。这是一个简单的Racket函数memo
,它接受一个函数并返回它的memoized版本:
(define (memo f)
(let ([table (make-hash)])
(lambda args
(hash-ref! table
args
(thunk (apply f args))))))
现在,我们可以递归地编写函数f
,而不必担心两次计算相同结果的性能问题,从而从指数时间算法下降到线性算法,同时保持实现简单:
(define f
(memo
(lambda (n)
(if (< n 4)
n
(+ (f (- n 1))
(* 2 (f (- n 2)))
(* 3 (f (- n 3)))
(* 4 (f (- n 4))))))))
请注意,只要函数f
存在,它就会在内存中保存一个表,其中包含每次调用它时的结果。
如果你想要一个正确的尾递归解决方案,你最好的方法可能是使用命名的let 结构。如果您执行(let name ([id val] ...) body ...)
,则在(name val ...)
中的任何位置调用body ...
将跳回let
的开头,并为绑定添加新值val ...
。一个例子:
(define (display-n string n)
(let loop ([i 0])
(when (< i n)
(display string)
(loop (add1 i)))))
使用它可以为你的问题提供一个尾递归解决方案,而不是定义一个辅助函数并调用它:
(define (f n)
(if (< n 4)
n
(let loop ([a 3] [b 2] [c 1] [d 0] [i 4])
(if (<= i n)
(loop (fn+1 a b c d) a b c (add1 i))
a))))
(define (fn+1 a b c d)
(+ a (* 2 b) (* 3 c) (* 4 d)))
此版本的函数会跟踪f
的四个值,然后使用它们计算下一个值并删除最旧的值。这构建了一个解决方案,同时只在内存中保留了四个值,并且它不会在调用之间保留一个巨大的表。 fn+1
辅助函数用于将函数的前四个结果组合到下一个结果中,它只是为了便于阅读。如果要优化内存使用,可能需要使用此功能。使用memoized版本有两个优点:
f(10)
然后调用f(4)
,则第二次调用将只是在常量时间内查找表,因为调用{ {1}}存储了调用f(10)
f
从0到10的所有结果。