我最近开始阅读SICP,我对将递归过程转换为尾递归形式非常感兴趣。
对于"一维"情况(线性的),如斐波那契数列或因子计算,转换并不困难。
例如,正如书中所说,我们可以按如下方式重写Fibonacci计算
(define (fib n)
(fib-iter 1 0 n))
(define (fib-iter a b count)
(if (= count 0)
b
(fib-iter (+ a b) a (- count 1))))
这种形式显然是尾递归
然而,对于"二维"情况,就像计算Pascal三角形(SICP中的Ex 1.12)一样,我们仍然可以轻松编写一个递归解决方案,如下所示
(define (pascal x y)
(cond ((or (<= x 0) (<= y 0) (< x y )) 0)
((or (= 1 y) (= x y) ) 1)
(else (+ (pascal (- x 1) y) (pascal (- x 1) (- y 1))))))
问题是,如何将其转换为尾递归形式?
答案 0 :(得分:3)
首先,递归过程pascal
过程可以用更简单的方式表示(假设非负的,有效的输入) - 像这样:
(define (pascal x y)
(if (or (zero? y) (= x y))
1
(+ (pascal (sub1 x) y)
(pascal (sub1 x) (sub1 y)))))
现在提出问题。 可以将递归过程实现转换为使用尾递归的迭代过程版本。但它比看起来更棘手,要完全理解它,你必须掌握动态编程的工作原理。有关此算法的详细说明,请参阅Steven Skiena的The Algorithm Design Manual,第2版,第278页。
这种算法并不适用于Scheme中的惯用解决方案,因为它要求我们将状态变为解决方案的一部分(在这种情况下,我们会更新部分结果在矢量)。这是一个相当人为的解决方案,我优化了表内存使用情况,因此一次只需要一行 - 而且这里是:
(define (pascal x y)
(let ([table (make-vector (add1 x) 1)])
(let outer ([i 1])
(when (<= i x)
(let inner ([j 1] [previous 1])
(when (< j i)
(let ([current (vector-ref table j)])
(vector-set! table j (+ current previous))
(inner (add1 j) current))))
(outer (add1 i))))
(vector-ref table y)))
事实上,在这种情况下,编写直接迭代,沿途改变变量会更自然。在Racket中,它的外观如下:
(define (pascal x y)
(define current null)
(define previous null)
(define table (make-vector (add1 x) 1))
(for ([i (in-range 1 (add1 x))])
(set! previous 1)
(for ([j (in-range 1 i)])
(set! current (vector-ref table j))
(vector-set! table j (+ (vector-ref table j) previous))
(set! previous current)))
(vector-ref table y))
我们可以打印结果并检查所示的所有三种实现是否有效。再次,在Racket:
(define (pascal-triangle n)
(for ([x (in-range 0 n)])
(for ([y (in-range 0 (add1 x))])
(printf "~a " (pascal x y)))
(newline)))
(pascal-triangle 5)
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
答案 1 :(得分:3)
更新:此问题有一个far easier math solution,您只能使用factorial来降低到O(行)。基于此,归结为:
(define (pascal-on row col)
(define (factorial from to acc)
(if (> from to)
acc
(factorial (+ 1 from) to (* acc from))))
(let* ((rmc (- row col))
(fac-rmc (factorial 1 rmc 1))
(fac-pos (factorial (+ rmc 1) col fac-rmc))
(fac-row (factorial (+ col 1) row fac-pos)))
(/ fac-row fac-pos fac-rmc)))
旧回答:
你需要研究模式。基本上你想从三角形的开头迭代,直到你有足够的信息来产生结果。显而易见的是,您需要前一行来计算下一行,因此必须是您提供的参数,如果请求的行不是当前行,它必须提供下一行。这个解决方案是尾部重复和快速闪电。
(define (pascal row col)
(define (aux tr tc prev acc)
(cond ((> tr row) #f) ; invalid input
((and (= col tc) (= row tr)) ; the next number is the answer
(+ (car prev) (cadr prev)))
((= tc tr) ; new row
(aux (+ tr 1) 1 (cons 1 acc) '(1)))
(else (aux tr ; new column
(+ tc 1)
(cdr prev)
(cons (+ (car prev) (cadr prev)) acc)))))
(if (or (zero? col) (= col row))
1
(aux 2 1 '(1 1) '(1))))
答案 2 :(得分:1)
要添加到Óscar's answer,我们可以使用continuation-passing style转换任何程序以使用尾调用:
;; Int Int (Int → Int) → Int
(define (pascal/k x y k)
(cond
[(or (<= x 0) (<= y 0) (< x y)) (k 0)]
[(or (= 1 y) (= x y)) (k 1)]
[else (pascal/k (- x 1) y
(λ (a)
(pascal/k (- x 1) (- y 1)
(λ (b) (k (+ a b))))))]))
;; Int Int → Int
(define (pascal x y) (pascal/k x y (λ (x) x)))
你可能会说这个程序并不那么令人满意,因为关闭会“增长”。但它们是在堆上分配的。在一般情况下,尾部调用的重点不在于性能,而在于空间安全:你不会破坏评估环境。