ruby递归优化 - 尾递归、增加栈

递归很好用,但容易导致栈溢出(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是支持尾递归的!

你可能感兴趣的:(ruby递归优化 - 尾递归、增加栈)