【问题标题】:Turning a recursive procedure to a iterative procedure - SICP exercise 1.16将递归过程转换为迭代过程 - SICP 练习 1.16
【发布时间】:2016-02-24 16:01:27
【问题描述】:

在计算机程序的结构和解释一书中,有一个使用连续平方计算指数的递归过程。

(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)))))

它工作得很好,但我绝对不明白如何自己达到这个解决方案。看起来非常聪明。

有人可以解释为什么迭代解决方案是这样的吗?解决这类问题的一般思路是什么?

2021 年更新: 去年,我完全忘记了这个练习和我看到的解决方案。我尝试解决它,最后我使用练习中提供的不变量作为转换状态变量的基础自己解决了它。我使用现在接受的答案来验证我的解决方案。谢谢@Óscar López。

【问题讨论】:

  • 大声笑,我花了 30 分钟才感到沮丧。你必须有惊人的毅力才能在这上面花一周时间。
  • @qiu 这取决于。平均而言,我花了一周到一个月的时间来解决本书中的一个练习。例如,我花了将近 7 个月的时间解决练习 1.11。我忍受了它,因为我几乎总是在解决它们之后学到一些东西。我给他们的时间最终总是值得的。这是一个例外,因为如果我发现一些无聊的事情,我实际上很容易分心和沮丧。

标签: loops recursion scheme sicp exponentiation


【解决方案1】:

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

(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,恕我直言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,然后继续调用 is n is even。我认为 2 * 是为了让第九个 2 相乘。但我在迭代版本中看不到 b* 。如果 n 是奇数,我真的很困惑会发生什么。
  • 相同的想法、相同的步骤、相同的算法,唯一改变的是我们传播解决方案的方式。在递归版本中,我们执行(* b (fast-expt …)),当我们返回函数调用堆栈时,解决方案得到传播。在迭代版本中,我们执行(* b acc),因为我们将先前调用(fast-expt …) 的结果存储在acc 中,并且解决方案在参数中传播,而不会产生必须在堆栈中返回的成本函数调用。
  • 我得到了你的例子。我知道另一个变量如何跟踪迭代过程中的更新值。不幸的是,如果 n 是奇数,我无法将其应用于指数解决方案。我知道我们为什么要 (* b acc) 将值放入累加器中,我认为我的困惑是因为在递归过程中,b 的值不会因每次调用而改变。但是对于迭代过程,b通过平方和平方变化很大。您介意提供另一个这样的示例吗?
猜你喜欢
  • 2017-01-19
  • 2013-06-19
  • 1970-01-01
  • 2017-09-12
  • 1970-01-01
  • 2019-02-10
  • 1970-01-01
  • 2011-10-29
  • 1970-01-01
相关资源
最近更新 更多