Loops 将递归过程转化为迭代过程-SICP练习1.16

Loops 将递归过程转化为迭代过程-SICP练习1.16,loops,recursion,scheme,sicp,Loops,Recursion,Scheme,Sicp,在《计算机程序的结构和解释》一书中,有一个使用逐次平方计算指数的递归过程 (define (fast-expt b n) (cond ((= n 0) 1) ((even? n) (square (fast-expt b (/ n 2)))) (else (* b (fast-expt b (- n 1)))))) 现在在练习1.16中: 练习1.16:设计一个使用连续平方和对数步数的迭代求幂过程的程序, fast-e

在《计算机程序的结构和解释》一书中,有一个使用逐次平方计算指数的递归过程

(define (fast-expt b n)
    (cond ((= n 0) 
        1)
    ((even? n) 
        (square (fast-expt b (/ n 2))))
   (else 
        (* b (fast-expt b (- n 1))))))
现在在练习1.16中:

练习1.16:设计一个使用连续平方和对数步数的迭代求幂过程的程序, fast-expt也是如此。(提示:使用 (b(^n/2))^2=(b(^2))^n/2 ,与指数n和基数b一起保留一个附加的状态变量a,并定义状态转换,以使乘积ab^n在不同状态之间保持不变。在过程开始时,a被取为1,而答案由过程结束时的a值给出。通常,定义在不同状态之间保持不变的不变量是考虑迭代算法设计的一种强有力的方式。) 我花了一个星期的时间,我完全不知道如何做这个迭代过程,所以我放弃了,并寻找解决方案。我找到的所有解决方案如下:

(define (fast-expt a b n)
    (cond ((= n 0) 
        a)
    ((even? n) 
        (fast-expt a (square b) (/ n 2)))
   (else 
        (fast-expt (* a b) b (- n 1)))))
现在,我明白了

        (fast-expt a (square b) (/ n 2)))
使用书中的提示,但是当n是奇数时,我的大脑爆炸了。在递归过程中,我得到了原因

        (* b (fast-expt b (- n 1))))))
工作。但是在迭代过程中,它变得完全不同

        (fast-expt (* a b) b (- n 1)))))
它工作得很好,但我完全不知道如何独自解决这个问题。它看起来非常聪明


有人能解释一下为什么迭代解是这样的吗?解决这类问题的一般方法是什么?

为了让事情更清楚,这里有一个稍微不同的实现,请注意,我使用了一个名为
loop
的助手过程来保留原始过程的arity:

(define (fast-expt b n)
  (define (loop b n acc)
    (cond ((zero? n) acc)
          ((even? n) (loop (* b b) (/ n 2) acc))
          (else (loop b (- n 1) (* b acc)))))
  (loop b n 1))
这里的
acc
是什么?它是一个用作结果累加器的参数(在书中,他们将此参数命名为
a
,IMHO
acc
是一个更具描述性的名称)。因此,在开始时,我们将
acc
设置为适当的值,然后在每次迭代中更新累加器,保持不变

一般来说,这是理解算法的迭代、尾部递归实现的“诀窍”:我们传递一个额外的参数和到目前为止计算的结果,并在到达递归的基本情况时返回它。顺便说一句,上面所示的迭代过程的通常实现是使用命名的
let
,这是完全等效的,编写起来更简单一些:

(define (fast-expt b n)
  (let loop ((b b) (n n) (acc 1))
    (cond ((zero? n) acc)
          ((even? n) (loop (* b b) (/ n 2) acc))
          (else (loop b (- n 1) (* b acc))))))

好的,我理解不变部分。但就解决方案而言,我认为迭代和递归过程的逻辑是等价的。这个问题的两个实现似乎没有相同的逻辑。我说的对吗?事实上他们的逻辑完全一样。两种解决方案最终都是将一个值乘以
b
,这一点不会改变。在递归版本中,我们用递归调用的结果乘以
b
,而在迭代版本中,递归调用的结果被累加到一个参数中,但这一切都是一样的-我们只是避免了等待递归返回,通过将其结果传递到一个参数中。想想在命令式语言中修改
for
循环中的局部变量时会做什么,这里也是一样,我们只是使用参数,就像使用局部变量一样。如果n是奇数,我仍然不明白。在递归版本中,如果n是奇数,我们用n-1进行b*另一个函数调用,现在n是偶数。我认为b*是为了增加一个基数。例如,若n为9,则执行2*fastExp 9-8,然后继续调用n为偶数。我想第九个2乘以2。但我在迭代版本中没有看到b*。如果n是奇数,我真的很困惑会发生什么。这是相同的想法,相同的步骤,相同的算法,唯一改变的是我们如何传播解。在递归版本中,我们执行
(*b(fast expt…)
,当我们在函数调用堆栈中返回时,解决方案就会传播。在迭代版本中,我们会执行
(*b acc)
,因为我们在
acc
中存储了先前调用
(fast expt…
)的结果,并且解决方案会在参数中传播,而不必在函数调用堆栈中返回。我得到了您的示例。我知道另一个变量如何在迭代过程中跟踪更新的值。不幸的是,如果n是奇数,我不能将它应用于指数解。我知道为什么我们要(*b acc)将值放入accumiliator,我想我的困惑是因为在递归过程中,b的值不会因每次调用而改变。但是对于迭代过程,b通过平方和平方变化很大。你介意再举一个这样的例子吗?