递归很好用,但容易导致栈溢出(SystemStackError: stack level too deep)。
简单的解决方案有两个,一个是把ruby的stack size调大,一个是用尾递归。
Stack level too deep
def factorial(n)
raise InvalidArgument, "negative input given" if n < 0
if n == 0
1
else
factorial(n - 1) * n
end
end
输入比较小的时候,代码工作正常
irb> factorial(1_000).to_s.size
=> 2568
但当我们把输入变大的时候,就会报错
irb> factorial(100_000).to_s.size
SystemStackError: stack level too deep
解决方案 - 增加栈大小
我们可以增加ruby vm的栈大小。只要设置RUBY_THREAD_VM_STACK_SIZE这个环境变量就可以了。
export RUBY_THREAD_VM_STACK_SIZE=50000000
我们还可以通过RubyVM::DEFAULT_PARAMS
这条命令查看原有的值。
不过,这种方式也不能完全解决问题的,更好的方式是,使用尾递归。
尾递归
ruby默认是不支持尾递归的,首先我们要先开启配置。
RubyVM::InstructionSequence.compile_option = {
tailcall_optimization: true,
trace_instruction: false
}
尾递归的实现如下:
def factorial_by_tail_call(n, accumulator = 1)
raise InvalidArgument, "negative input given" if n < 0
return accumulator if n == 0
return factorial_by_tail_call(n - 1, accumulator * n)
end
尾递归为什么没有溢出?
简单来说在factorial内, factorial(n - 1) * n
必须要有n才能计算出结果,所以当前这次执行(procedure call)必须要存在内存里。
但factorial_by_tail_call
却不一样,factorial_by_tail_call(n - 1, accumulator * n)
不依赖任何其他变量,可以直接被执行,其实没必要存在内存里,所以可以被优化。
语言对尾递归的支持情况
首先尾递归优化需要解释器的支持。
ruby也是后来才支持的,但需要手动开启。
java不支持,clojure虽然不支持,但用recur达到了同样的效果。
最后,上个世纪的lisp是支持尾递归的!