【问题标题】:Understanding behaviour of recursive code理解递归代码的行为
【发布时间】:2017-04-29 13:43:20
【问题描述】:

我正在检查计划在几周内回答的考试的旧试卷,但以下问题让我受阻:

有限的位序列表示为具有来自集合 {0,1} 的值的列表 - 例如,[0,1,0]、[1,0,1,1]。

[ ] 表示空列表,[b] 是由一位 b 组成的列表。对于非空列表 l,head(l) 返回 l 的第一个元素,tail(l) 返回从 l 中删除第一个元素得到的列表。 p>

a:l 表示在列表 l 的头部添加 a 形成的新列表。

例如:

• 头([0,1,0]) = 0, 尾([0,1,0]) = [1,0],

• 头([1]) = 1,尾([1]) = [ ],并且

• 1:[0,1,0] = [1,0,1,0]。

考虑以下函数:

f1 将一个列表作为输入并返回另一个列表。

f1(s):
 if (s == []) then return([1])
  else if (head(s) == 0) then return(1:tail(s))
  else if (head(s) == 1) then return(0:f1(tail(s)))
 endif

f2 将一个位和一个列表作为输入并返回一个位。

f2(b,s):
 if (s == []) then return(b)
  else if (head(s) == 0) then return(f2(not(b),tail(s)))
  else if (head(s) == 1) then return(not(b))
 endif

g1 将一个非负数作为输入并返回一个列表。

g1(n):
 if (n == 0) then return([0])
  else return f1(g1(n-1))
 endif

g2 将一个非负数作为输入并返回一个位。

g2(n):
 if (n == 0) then return(0)
  else return f2(g2(n-1),g1(n))
 endif
  1. g2(7) 和 g2(8) 的值是多少?
  2. g2(256) 和 g2(257) 的值是多少?

到目前为止,我只能理解 f1 的行为(将 head 替换为 1,如果它是 0 并返回。否则,将 head 替换为 0 并在其余部分调用自身列表。)和 g1(n)f1 应用于 [0] n 次(f1(f1(. ..f1([0]))) - n 次)。

是否有一些结构化的方法可以解决这个问题和类似问题?(特别是在时间压力下。)

编辑:f2 反转位 b 的次数是第一个 1 之前的 0 的数量列表

g2() 仍然是个谜。

【问题讨论】:

  • 到底是什么问题?
  • 我不会称这段代码为混淆,也不是伪代码。这个练习的重点是阅读代码并成为一台人机。诀窍是一旦你看到计算是如何工作的,你就不必做所有的 256 或 257 循环。你只需要开始评估,观察一个模式,然后基本上跳到最后。
  • @changed 问题是这是一道考试题,测试大量输入是不可行的,因为可用时间最多为 30 分钟(最多)。
  • 一个好的开始是自己完成这些答案,无论在考试中这样做有多慢或不可行。没有魔术,你只需要练习,直到它变得更快。
  • 看了你的回答,你不懂f1,你只是把代码变成文字。你必须看到模式并概括它在做什么。但是很抱歉,这样做仍然没有技巧。

标签: algorithm recursion


【解决方案1】:

纸笔评估

最好的方法就是开始。获取您面前的功能定义并开始您的笔和纸评估。这个问题要求我们比较g2(7)g2(8),但看看g2,我们需要了解f2g1,所以让我们从那里开始。因为g1的值是先计算出来的,所以我们先熟悉g1

我将开始将一些值推入其中并获得结果。我将从0 开始,因为在查看g1 之后,这似乎是最容易计算的

g1(0) => [0]

g1(1) => f1(g1(0)) => ... stop, do not recompute `g1(0)` here, we already know the answer above
      => f1([0])
      => [1]

g1(2) => f1(g1(1)) => we already know `g1(1)` from above
      => f1([1])
      => 0 : f1([])
      => [0,1]

g1(3) => f1(g1(2)) => we already know `g1(2)`
      => f1([0,1])
      => [1,1]

g1(4) => f1(g1(3)) => reuse `g1(3)`
      => f1([1,1])
      => 0 : f([1]) => we already know `f1([1])`
      => 0 : [0,1]
      => [0,0,1]

我已经在这里看到了一个模式。你? g1 正在为n 中的little endian bit order 生成1 和0 的二进制序列

g1(0) => [0]
g1(1) => [1]
g1(2) => [0,1]
g1(3) => [1,1]
g1(4) => [0,0,1]
g1(7) => ?
g1(8) => ?

我的猜测分别是[1,1,1][0,0,0,1]。让我们继续看看我们是否正确......

g1(5) => f1(g1(4))
      => f1([0,0,1])
      => [1,0,1]

g1(6) => f1(g1(5))
      => f1([1,0,1])
      => [0,1,1]

g1(7) => f1(g1(6))
      => f1([0,1,1])
      => [1,1,1]

g1(8) => f1(g1(7))
      => f1([1,1,1])
      => 0 : f([1,1]) => we already know f1([1,1])
      => 0 : [0,0,1]
      => [0,0,0,1]

进展顺利

嘿,我们的猜测是正确的!评估 g(0) 到 g(8) 中的每一个应该需要大约 30-45 秒——大约需要 5.5 分钟,我们非常了解 g1f1 的工作原理。我们对g1 的理解要好一些,因为我们知道它只需要一个数字并吐出二进制位序列来表示输入数字。 f1如何 确切地工作方面有点神奇,但最酷的部分是它并不重要——问题要求我们比较 g2 的值,只要我们可以计算g2 的值,如果我们对其他函数有很好的了解也没关系。

上次评估 g1 让我们对 f1 有了一些有价值的见解,看起来与 f2 相关的 g2 的情况相同。现在我们有了一些可以引用的g1 的值,让我们尝试计算g2 的一些值——我会像上次一样从0 开始

g2(0) => 0

g2(1) => f2(g2(0),g1(1)) => we already know `g2(0)` and `g1(1)`
      => f2(0, [1])
      => 1

g2(2) => f2(g2(1), g1(2)) => we already know these!
      => f2(1, [0,1])
      => f2(0, [1])
      => 1

g2(3) => f2(g2(2), g1(3))
      => f2(1, [1,1])
      => 0

g2(4) => f2(g2(3), g1(4))
      => f2(0, [0,0,1])
      => f2(1, [0,1])
      => f2(0, [1])
      => 1

在这一点上,我还没有看到很多的模式。起初我在想g2 可能会告诉我们给定的整数是偶数还是奇数,但事实并非如此。我在想的唯一另一件事是它可能返回 1 用于 2 的幂和 0 用于非 2 的幂。这将是一个奇怪的函数。让我们继续找出答案

g2(5) => f2(g2(4), g1(5))
      => f2(1, [1,0,1])
      => 0

g2(6) => f2(g2(5), g1(6))
      => f2(0, [0,1,1])
      => f2(1, [1,1])
      => 0

g2(7) => f2(g2(6), g1(7))
      => f2(0, [1,1,1])
      => 1

g2(8) => f2(g2(7), g1(8))
      => f2(1, [0,0,0,1])
      => f2(0, [0,0,1])
      => f2(1, [0,1])
      => f2(0, [1])
      => 1

出现了意想不到的模式

好的,我们已经联系上了g2(7) == 1g2(8) == 1。我们的 2 次幂理论肯定没有成功,但这没关系,因为我们可以看到出现了另一种模式 - 如果位序列包含奇数个 1s,g2 将返回 1,它会如果位序列包含偶数个1s,则返回0

我在这里做了一个小真值表来检查我的猜测

g1(0) => [0]          ones(0) => 0    odd?(ones(0)) => 0    g2(0) => 0
g1(1) => [1]          ones(1) => 1    odd?(ones(1)) => 1    g2(1) => 1
g1(2) => [0,1]        ones(2) => 1    odd?(ones(2)) => 1    g2(2) => 1
g1(3) => [1,1]        ones(3) => 2    odd?(ones(3)) => 0    g2(3) => 0
g1(4) => [0,0,1]      ones(4) => 1    odd?(ones(4)) => 1    g2(4) => 1
g1(5) => [1,0,1]      ones(5) => 2    odd?(ones(5)) => 0    g2(5) => 0
g1(6) => [0,1,1]      ones(6) => 2    odd?(ones(6)) => 0    g2(6) => 0
g1(7) => [1,1,1]      ones(7) => 3    odd?(ones(7)) => 1    g2(7) => 1
g1(8) => [0,0,0,1]    ones(8) => 1    odd?(ones(8)) => 1    g2(8) => 1

所以g2(x) 等价于odd?ones(x),至少在0 <= x <= 8 的地方。评估 g2(0)g2(8) 大约需要 10 分钟,分析模式可能需要 5-10 分钟,总共运行大约 25 分钟。但是现在我们知道了评估 g(256)g(257) 所需的一切,而无需进行所有繁琐的逐步递归

代码已被破解

将 256 转换为二进制,我们知道 g1(256)[0,0,0,0,0,0,0,0,1],同样g1(257)[1,0,0,0,0,0,0,0,1] 所以很容易计算 g2(256) 并将其与 g(257) 现在进行比较

g1(256) => [0,0,0,0,0,0,0,0,1]    ones(256) => 1    odd?(ones(256)) => 1    g2(256) => 1
g1(257) => [1,0,0,0,0,0,0,0,1]    ones(257) => 2    odd?(ones(257)) => 0    g2(257) => 0

仅此而已

g2(7) => 1
g2(8) => 1
g2(256) => 1
g2(257) => 0

为此增加几分钟,我们应该在 30 分钟左右 - 根据熟练程度和识别模式的能力,给予或收取 50%。


选择一种模式,任何模式

哦,嗯,也许你会发现一个不同的模式——这完全没问题!重要的是你要尽你最大的能力去检查它的真实性(记住你的时间限制)。

f2 反转位 b 的次数与列表 s 中第一个 1 之前的 0 的数量相同。

我看到您对f2 有一些了解,但它有点问题,因为b 最初等于g2(n - 1)。假设我们要计算f2(b, s),其中bg2(1234)...我们被卡住了,因为我们还不知道那是什么。不幸的是,这是对f2的死胡同观察

这就是为什么我能够(或幸运地)在g1g2 之间建立与f1 或@987654392 完全无关的已知 输入的相关性。 @。一旦我能够看到g1g2 是如何工作的,我什至不必计算f1f2 来评估g(256)g(257)。这是一个巨大的收获,因为我有效地将f1f2 从我的笔和纸评估模型中剔除,并且仍然得出g1g2 的正确答案。如果没有达到这一点,我会一直在寻找另一个相关性或手动评估一直到g(257) ......这对于考试来说会花费太长时间。

【讨论】:

  • 这绝不是一个小问题。我有很多直觉,但希望你的教科书或老师能够为你提供有效解决这些问题的技巧。我用来剖析递归函数的一件大事是从一个导致最少计算的输入值开始(例如,就像我们对g1(0) 所做的那样)——然后继续寻找下一个最容易计算的输入(在我们的例子是g1(1),因为我们已经知道g1(0)的结果)
  • 我使用了同样的技术来拆解g2,幸运的是它奏效了——有时它确实有效,而其他时候你可能需要尝试以另一种方式接近这个函数。无论哪种方式,对于这个特定的问题,我认为一个新手程序员应该能够在 30-60 分钟内理解 4 个函数形成的关系。如果您有任何其他问题,我很乐意尽我所能提供帮助^_^
  • 这是一个很好的答案;我非常感谢您为此付出的努力。我在做自下而上的功能工作效率低下,但这不仅更快,而且提高了清晰度。非常感谢。
猜你喜欢
  • 2020-10-09
  • 2014-06-16
  • 2020-12-23
  • 2016-08-07
  • 2016-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-01-20
相关资源
最近更新 更多