Ruby 为什么向递归过程调用添加1会返回调用的计数?

Ruby 为什么向递归过程调用添加1会返回调用的计数?,ruby,recursion,Ruby,Recursion,假设我有以下代码: def rcall(num) return 0 if 10 == num 1 + rcall(num - 1) end p rcall(90) # => 80 此代码返回的值总是比传递到num中的值小10,即递归调用的计数 我看不出它是怎么工作的。我有一个模糊的理解,如果满足退出条件,我们返回零,这样就不会再次增加计数器。但是,在proc调用中添加一个函数究竟是如何增加调用次数的呢?我看不出递增器在哪里累积 另外,这是Ruby体系结构特有的技术,还是更普遍地

假设我有以下代码:

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

p rcall(90) # => 80
此代码返回的值总是比传递到
num
中的值小10,即递归调用的计数

我看不出它是怎么工作的。我有一个模糊的理解,如果满足退出条件,我们返回零,这样就不会再次增加计数器。但是,在proc调用中添加一个函数究竟是如何增加调用次数的呢?我看不出递增器在哪里累积


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

假设您将基本条件更改为
如果num.zero返回0?
,然后使用参数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
虽然这可能需要一些其他设置,但我不是很确定


参见

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

任何给定的调用在再次调用该方法时都会隐藏其当前状态,因为它需要其中的值才能继续。只有当最深级别返回一个值(在本例中为0)时,整个过程才会展开,因为以前的调用现在有值要处理(每个值加1)。因为使用10作为“完成”值,实际上跳过了比保护比较小的任何值(例如,将比较设置为0或负数),所以得到的值减少了10


在这种情况下,guard测试避免了“堆栈级别太深”错误,因为没有任何其他方法可以防止失控递归-您可以看到,通过使用小于比较值的初始值,例如,当比较为10时,从5开始。我刚刚在代码中添加了一些
put
,也许这有助于更好地遵循逻辑,而不是我用英语解释

这是经过调整的代码:

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)

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

其他语言支持递归,例如Python。这里是著名的斐波那契()


我还想在YouTube上分享这段关于递归的电脑爱好者视频:

,这通常是不正确的。 重要的一点是,
rcall
在不执行递归调用时总是返回0。 因此,当您递归调用它时,可以将1添加到
rcall
。 每次递归调用都会得到一个+1,当递归调用停止时,+1链停止

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

def keep_counting
  2 + keep_counting + keep_counting
end 

我建议您通过调试器运行它,这样您就可以看到调用堆栈和相关变量了。@jhpratt抱歉,但是什么相关变量?唯一的一个,
num
,一直在尽职尽责地递减。调用堆栈只会说“test.rb:6:inrcall'”,`每次递归一次,不管我是否向调用中添加一个递归。如果从退出条件开始,就会更容易:调用
rcall(10)
时,满足第一个条件,返回值为
0
。调用
rcall(11)
时,第一个条件不满足,返回值为
1+rcall(10)
,计算结果为
1+0
。同样地,
rcall(12)
1+rcall(11)
=
1+(1+rcall(10))
=
1+1+0
等等……哦,好吧,见鬼。返回值是1加上下一次调用的返回值,即1,依此类推。因此,我们说的是返回每个调用的
1
,并将其添加到所有其他调用返回的所有其他
1
。最后一个返回零,因为我们已经有了想要的计数——通常是。好的,越来越清楚了。(我明白了吗?){tailcall\u optimization:true,trace\u指令:false},否则上述内容将很快耗尽堆栈。@AlekseiMatiushkin在OP的代码中,最后一条“指令”是加法,而不是方法调用。为了使TCO工作,最后一个表达式必须是
rcall(…)
,而不是
1+rcall(…)
(这显然需要一个不同的实现)。@Stefan确实如此。
rcall
应该接受两个参数并对自身进行求和。它不仅效率更高,而且允许技术上无限递归,而上述操作会导致上的堆栈溢出≈第200次迭代。是的,我曾经在VB6中使用过一次,用来计算“符号和驱动”汽车租赁的付款是在一个付款应纳税而预付费用不应纳税的州进行的。这是一个微积分问题,你必须不断地重新计算并迭代到最接近的一分钱,这是递归的一个很好的用例。看起来最模糊的事情是,一旦警卫条件发生变化,将所有的费用汇总在一起的过程我想问的是,在
def keep_counting
  2 + keep_counting + keep_counting
end