I saw three different ways to write the recursive form of a Fibonacci function: With the math inline, With the math inline with result caching and one Using tail recursion with. I understand using memoization converts an O(N) algorithm to an O(1) after the answers are cached. But I fail to understand how tail call optimization can help this much. I was under the impression it might prevent a few copies or something like that. It is nearly as fast as the O(1). What is Ruby doing that makes this so fast?
Here is the slow naive implementation with math inline. It is clearly the slowest running O(N) time, and then when looped as in the display in O(N^2) time.
puts Benchmark.measure {
# Calculate the nth Fibonacci number, f(n).
def fibo (n)
if n <= 1
return n
else
value = fibo(n-1) + fibo(n-2)
return value
end
end
# Display the Fibonacci sequence.
(1..40).each do |number|
puts "fibo(#{number}) = #{fibo(number)}"
end
}
Times Ruby 1.9.3: 55.989000 0.000000 55.989000 ( 55.990000)
Times JRuby 1.7.9: 51.629000 0.000000 51.629000 ( 51.629000)
source( http://rayhightower.com/blog/2014/04/12/recursion-and-memoization/?utm_source=rubyweekly )
Here is the version which memoizes answers, it is clear why this is fast to me. Once it has done the math any following request runs in O(1) time, so when it included in a loop it still runs in O(N) time in the worst case:
puts Benchmark.measure {
# Fibonacci numbers WITH memoization.
# Initialize the memoization array.
@scratchpad = []
@max_fibo_size = 50
(1..@max_fibo_size).each do |i|
@scratchpad[i] = :notcalculated
end
# Calculate the nth Fibonacci number, f(n).
def fibo (n)
if n > @max_fibo_size
return "n must be #{@max_fibo_size} or less."
elsif n <= 1
return n
elsif @scratchpad[n] != :notcalculated
return @scratchpad[n]
else
@scratchpad[n] = fibo(n-1) + fibo(n-2)
return @scratchpad[n]
end
end
# Display the Fibonacci sequence.
(1..40).each { |number|
puts "fibo(#{number}) = #{fibo(number)}"
}
}
Times Ruby 1.9.3: 0.000000 0.000000 0.000000 ( 0.025000)
Times JRuby 1.7.9: 0.027000 0.000000 0.027000 ( 0.028000)
Source( http://rayhightower.com/blog/2014/04/12/recursion-and-memoization/?utm_source=rubyweekly )
The version tail call recursion version of this runs pretty much instantly:
puts Benchmark.measure {
# Calculate the nth Fibonacci number, f(n). Using invariants
def fibo_tr(n, acc1, acc2)
if n == 0
0
elsif n < 2
acc2
else
return fibo_tr(n - 1, acc2, acc2 + acc1)
end
end
def fibo (n)
fibo_tr(n, 0, 1)
end
# Display the Fibonacci sequence.
(1..50).each do |number|
puts "fibo(#{number}) = #{fibo(number)}"
end
}
Times Ruby 1.9.3: 0.000000 0.000000 0.000000 ( 0.021000)
Times JRuby 1.7.9: 0.041000 0.000000 0.041000 ( 0.041000)
Source ( https://gist.github.com/mvidaurre/11006570 )