为什么在递归proc调用中加1会返回进行的调用计数?

时间:2019-03-15 03:55:27

标签: ruby recursion

假设我有以下代码:

def rcall(num)
  return 0 if 10 == num
  1 + rcall(num - 1)
end

p rcall(90) # => 80

此代码将始终返回比传递给num的值小10的值,即进行的递归调用的计数。

我看不到它是如何工作的。我有一个模糊的理解,如果满足退出条件,我们将返回零,以免再次增加计数器。但是,确切地讲,在proc调用中加1会如何增加调用的次数?我看不到增量器在哪里累积。

此外,这是特定于Ruby体系结构的技术,还是更普遍地适用?在回答有关如何计算递归调用的问题的任何答案中,我都没有看到它。似乎大多数时候,人们都会传递一个计数器变量来跟踪计数。

4 个答案:

答案 0 :(得分:3)

假设您将基本条件更改为return 0 if num.zero?,然后使用参数3调用它,如下所示:

1 + rcall(2)
    # => 1 + rcall(1)
             # => 1 + rcall(0)
                      # => 0

如果您用其结果替换rcall调用(从底部开始),则会显示到1 + 1 + 1 + 0

换句话说,从基础案例开始,您可以更轻松地理解它:

rcall(0)
# 0

rcall(1)
# the same as 1 + rcall(0), which is one

rcall(2)
# the same as 1 + rcall(1), which is two.

希望您能看到图案。

正如评论中提到的,您可以像这样对它进行优化:

RubyVM::InstructionSequence.compile_option = {
  tailcall_optimization: true,
  trace_instruction: false
}

def rcall(num, sum=0)
  return sum if 10 == num
  rcall(num - 1, sum + 1)
end

尽管这可能需要其他设置,但我不确定。

请参见What Is Tail Call Optimization?

答案 1 :(得分:1)

递归是很多人都知道的事情之一,但是没有多少人可以形容它值得该死(包括我自己)。这是几种语言(不仅仅是Ruby)支持的一种通用编程方法。

任何给定的调用都会在再次调用该方法时隐藏其当前状态,因为它需要一个值来继续。只有最深的级别返回一个值(在这种情况下为0)时,整个东西才能解开,因为先前的调用现在具有可以使用的值(每个值加1)。之所以减少10,是因为您使用10作为“完成”值,实际上跳过了所有比保护比较小的值(例如,将比较设置为0或负数)。

在这种情况下,保护测试避免了“堆栈级别太深”错误,因为没有其他方法可以防止递归失控-您可以看到,使用小于比较值的初始值,例如开始比较为10时为5。

答案 2 :(得分:1)

我刚刚在您的代码中添加了puts,也许比我用英语能解释的更好地遵循了逻辑。

因此,这是经过调整的代码:

def rcall(num)
  if 10 == num
    rec = 0
    puts "rec = #{rec} ---- End of recursion"
    return rec
  end
  rec = rcall(num - 1)
  res = 1 + rec
  puts "rec = #{rec} \t\t res = i + rec = #{res}"
  res
end

例如,当您致电15时,您会得到: rcall(15)

# rec = 0 ---- End of recursion
# rec = 0      res = i + rec = 1
# rec = 1      res = i + rec = 2
# rec = 2      res = i + rec = 3
# rec = 3      res = i + rec = 4
# rec = 4      res = i + rec = 5
# 5

如果调用的数字小于10,则永远不会到达递归的结尾,因此不会返回任何值来构建“调用堆栈”,并且会引发错误:stack level too deep (SystemStackError)


其他语言也支持递归,例如Python。在著名的斐波那契(How to write the Fibonacci Sequence?)上。

我还想分享以下有关在YouTube上递归的 Computerphile视频https://youtu.be/Mv9NEXX1VHc

答案 3 :(得分:1)

通常情况并非如此。 重要的部分是rcall在不进行递归调用时始终返回0。 因此,您可以在递归调用时将1加到rcall上。 您为每个递归调用获得+1,并且当递归调用停止时+1链也会停止。

这也可以计算递归调用的数量,但永远不会停止计数:

def keep_counting
  2 + keep_counting + keep_counting
end