On Jul 14, 2006, at 4:32 AM, Glenn Cadman wrote:

> Daniel Martin wrote:
>> "Erik Veenstra" <erikveen / gmail.com> writes:
>>
>>> You could use "case" as well (see version 2). It's faster.
>>> Using an ordinary if..else (without the elsif part) is faster
>>> (see version 3). And version 4 is once again faster.
>>>
>>> 4 is faster than 3 is faster than 2 is faster than 1...
>>
>> All of these suffer from the problem that they make approximately  
>> fib(n)
>> function calls to compute fib(n).  Why not remember the value of
>> fib(n) the ruby way, with a Hash that computes it naturally?
>>
>> $fib = Hash.new{|h,k| h[k] = h[k-2] + h[k-1]}
>> $fib[0] = 0
>> $fib[1] = 1
>>
>> def fib(n)
>>   $fib[n]
>> end
>>
>> This will be faster, and O(n), rather than O(fib(n)).  Also note that
>> the base cases of 0 and 1 are handled simply and declaratively  
>> without
>> messing up the main body of the code.
>>
>> (Of course, now someone will respond with one of the O(log(n))
>> algorithms for computing fib(n))
>
> Well whilst my orginal exercise was just to get recursion working in
> ruby, I have to say this solution is very quick to calculate any  
> result
> and is recursive in a way that is quite "foriegn" to me. The  
> problem for
> me is that I find it difficult to understand what is happening in this
> code. I would have though the stack would collapse due to referencing
> something that is not defined yet. eg.  h[k-2] for example in the  
> intial
> state of the $fib hash would not be found unless the k was 3 or 2. eg
> $fib(30) would return "I dont have any thing to reference for h[30] =
> h[28] + h[29], has h[28] and h[29] havent been defined.
>
> BTW How would I then access $fib to puts an output of the sequence  
> eg. 0
> 1 1 2 3 5 8 13 etc.

(I hate when the email doesn't get through)

I first saw this Hash trick in a RubyQuiz solution.  I've used it in  
different ways since, but here's some amazing evidence as to it's power:

I ran the comparisons locally to try things out.  Basically, you just  
can't beat memoization with a Hash for something this simple (it's  
just addition after all).  I did adjust some of the implementations  
so that they produced the same sequence: fib(0) = 0, fib(1) = 1, fib 
(2) = 1, fib(3) = 2, etc.  (Some of them had fib(0) == fib(1) == 1)


100.times { 0.upto(30) { |n| fib(n) } }
                          user     system      total        real
fib1.rb: if        682.070000   1.850000 683.920000 (689.604829)
fib2.rb: case      631.260000   1.380000 632.640000 (636.994644)
fib3.rb: simple if 512.770000   0.860000 513.630000 (515.151776)
fib4.rb: simple ?: 498.760000   0.470000 499.230000 (499.626195)
fib6.rb: Hash        0.000000   0.000000   0.000000 (  0.007612)
fib7.rb: formula     0.030000   0.000000   0.030000 (  0.023598)
fib8.rb: BigMath     2.750000   0.010000   2.760000 (  2.762493)

                          user     system      total        real
fib1.rb: if        100.000 %    1.850000 683.920000 (689.604829)
fib2.rb: case       92.550 %    1.380000 632.640000 (636.994644)
fib3.rb: simple if  75.178 %    0.860000 513.630000 (515.151776)
fib4.rb: simple ?:  73.124 %    0.470000 499.230000 (499.626195)
fib6.rb: Hash        0.000 %    0.000000   0.000000 (  0.007612)
fib7.rb: formula     0.004 %    0.000000   0.030000 (  0.023598)
fib8.rb: BigMath     0.403 %    0.010000   2.760000 (  2.762493)


Let's peel those fast ones apart a bit:

10_000.times { 0.upto(30) { |n| fib(n) } }
                        user     system      total        real
fib6.rb: Hash      0.240000   0.000000   0.240000 (  0.246511)
fib7.rb: formula   2.610000   0.010000   2.620000 (  2.635959)
fib8.rb: BigMath 302.950000   0.810000 303.760000 (306.202394)


What about when the numbers get bigger? Does the formula start to  
have an advantage?

1000.times { 0.upto(300) { |n| fib(n) } }
                        user     system      total        real
fib6.rb: Hash      2.310000   0.010000   2.320000 (  2.322534)
fib7.rb: formula  45.720000   0.090000  45.810000 ( 46.018314)

Uh, not really.


Let's see them all together again:

fib1.rb: if for 30: 832040
fib2.rb: case for 30: 832040
fib3.rb: simple if for 30: 832040
fib4.rb: simple ?: for 30: 832040
fib6.rb: Hash for 30: 832040
fib7.rb: formula for 30: 832040
fib8.rb: BigMath for 30: 832040
1000.times { 0.upto(30) { |n| fib(n) } }
                          user     system      total        real
fib1.rb: if        6158.150000   3.740000 6161.890000 (6167.126104)
fib2.rb: case      5702.300000   5.630000 5707.930000 (5722.498983)
fib3.rb: simple if 4837.140000   3.320000 4840.460000 (4846.447905)
fib4.rb: simple ?: 4969.330000   5.540000 4974.870000 (4986.140834)
fib6.rb: Hash        0.020000   0.000000   0.020000 (  0.021521)
fib7.rb: formula     0.240000   0.000000   0.240000 (  0.245543)
fib8.rb: BigMath    27.740000   0.040000  27.780000 ( 27.828623)

==> fib.rb <==
require 'benchmark'
include Benchmark

require '../constantize.rb'

fibsto = ENV['FIBS_UPTO'] || 30
fibrep = ENV['FIB_REPTS'] || 1000

algorithms = [ ]
Dir.glob(ARGV.empty? ?
          'fib[1-46-9].rb' :
          ARGV.each { |p| Regexp.quote(p) }.unshift('{').push 
('}').join(%{,})) do |f|

   require f
   mod = constantize(File.basename(f,'.rb').capitalize)
   include mod
   algorithms << Hash[ :description => "#{f}: #{mod.module_eval  
{ description }}",
                       :alg => lambda { fibrep.times { 0.upto(fibsto)  
{ |n| mod.fib(n) } } },
                       :fib => lambda { |n| mod.fib(n) }
                     ]
end

algorithms.each do |a|
   fibsto.upto(fibsto) do |n|
     puts "#{a[:description]} for #{n}: #{a[:fib].call(n)}"
   end
end

puts "#{fibrep}.times { 0.upto(#{fibsto}) { |n| fib(n) } }"
bm(1 + algorithms.inject(9) { |mx,h| mx<h[:description].length ? h 
[:description].length : mx }) do |x|
   algorithms.each do |a|
     x.report(a[:description]) { a[:alg].call }
   end
end

==> fib1.rb <==
# VERSION 1
module Fib1
   def description; "if"; end

   def self.fib(n)
     # puts "#{File.basename(__FILE__,'.rb')}(#{n})"
     if n == 0
       return 0
     elsif n == 1
       return 1
     else
       return fib(n-1) + fib(n-2)
     end
   end
end

==> fib2.rb <==
# VERSION 2
module Fib2
   def description; "case"; end

   def self.fib(n)
     # puts "#{File.basename(__FILE__,'.rb')}(#{n})"
     case n
     when 0
       0
     when 1
       1
     else
       fib(n-1) + fib(n-2)
     end
   end
end

==> fib3.rb <==
# VERSION 3
module Fib3
   def description; "simple if"; end

   def self.fib(n)
     # puts "#{File.basename(__FILE__,'.rb')}(#{n})"
     if n<=1
       n.zero? ? 0 : 1
     else
       fib(n-1) + fib(n-2)
     end
   end
end

==> fib4.rb <==
# VERSION 4
module Fib4
   def description; "simple ?:"; end

   def self.fib(n)
     # puts "#{File.basename(__FILE__,'.rb')}(#{n})"
     n<=1 ? (n.zero? ? 0 : 1) : fib(n-1) + fib(n-2)
   end
end

==> fib5.rb <==
# Version 5
module Fib5
   def description; "lambda"; end

   fib = lambda{|n|
     # puts "#{File.basename(__FILE__,'.rb')}(#{n})"
     n<=1 ? (n.zero? ? 0 : 1) : fib[n-1] + fib[n-2]
   }
end

==> fib6.rb <==
# Version 6
module Fib6
   def description; "Hash"; end

   $fib = Hash.new{|h,k| h[k] = h[k-2] + h[k-1]}
   $fib[0] = 0
   $fib[1] = 1

   def self.fib(n)
     # puts "#{File.basename(__FILE__,'.rb')}(#{n})"
     # puts "#{File.basename(__FILE__,'.rb')}(#{n}) => #{$fib[n]}" if  
n == 30
     $fib[n]
   end
end

==> fib7.rb <==
# Version 7
# Weisstein, Eric W. "Binet's Fibonacci Number Formula." From  
MathWorld--A Wolfram Web Resource.
# http://mathworld.wolfram.com/BinetsFibonacciNumberFormula.html

module Fib7
   def description; "formula"; end

   SQRT5 = Math.sqrt(5)
   def self.fib(n)
     # puts "#{File.basename(__FILE__,'.rb')}(#{n})"
     (((1+SQRT5)**n - (1-SQRT5)**n)/(2**n * SQRT5)).round
   end
end

==> fib8.rb <==
# Version 8
# Weisstein, Eric W. "Binet's Fibonacci Number Formula." From  
MathWorld--A Wolfram Web Resource.
# http://mathworld.wolfram.com/BinetsFibonacciNumberFormula.html

require 'bigdecimal'
require 'bigdecimal/math'
include BigMath

module Fib8
   def description; "BigMath"; end

   SQRT5 = BigMath.sqrt(BigDecimal("5"),20)
   def self.fib(n)
     # puts "#{File.basename(__FILE__,'.rb')}(#{n})"
     (((1+SQRT5)**n - (1-SQRT5)**n)/(2**n * SQRT5)).round.to_i
   end
end

==> ../constantize.rb <==
   # from Jim Weirich (based on email correspondence)
   def constantize(camel_cased_word)
     camel_cased_word.
       sub(/^::/,'').
       split("::").
       inject(Object) { |scope, name| scope.const_get(name) }
   end

==> timing.txt <==
                          user     system      total        real
fib1.rb: if        682.070000   1.850000 683.920000 (689.604829)
fib2.rb: case      631.260000   1.380000 632.640000 (636.994644)
fib3.rb: simple if 512.770000   0.860000 513.630000 (515.151776)
fib4.rb: simple ?: 498.760000   0.470000 499.230000 (499.626195)
fib6.rb: Hash        0.000000   0.000000   0.000000 (  0.007612)
fib7.rb: formula     0.030000   0.000000   0.030000 (  0.023598)
fib8.rb: BigMath     2.750000   0.010000   2.760000 (  2.762493)

                          user     system      total        real
fib1.rb: if        100.000 %    1.850000 683.920000 (689.604829)
fib2.rb: case       92.550 %    1.380000 632.640000 (636.994644)
fib3.rb: simple if  75.178 %    0.860000 513.630000 (515.151776)
fib4.rb: simple ?:  73.124 %    0.470000 499.230000 (499.626195)
fib6.rb: Hash        0.000 %    0.000000   0.000000 (  0.007612)
fib7.rb: formula     0.004 %    0.000000   0.030000 (  0.023598)
fib8.rb: BigMath     0.403 %    0.010000   2.760000 (  2.762493)

10_000.times
                        user     system      total        real
fib6.rb: Hash      0.240000   0.000000   0.240000 (  0.246511)
fib7.rb: formula   2.610000   0.010000   2.620000 (  2.635959)
fib8.rb: BigMath 302.950000   0.810000 303.760000 (306.202394)

0.upto(300)
                        user     system      total        real
fib6.rb: Hash      2.310000   0.010000   2.320000 (  2.322534)
fib7.rb: formula  45.720000   0.090000  45.810000 ( 46.018314)

fib1.rb: if for 30: 832040
fib2.rb: case for 30: 832040
fib3.rb: simple if for 30: 832040
fib4.rb: simple ?: for 30: 832040
fib6.rb: Hash for 30: 832040
fib7.rb: formula for 30: 832040
fib8.rb: BigMath for 30: 832040
1000.times { 0.upto(30) { |n| fib(n) } }
                          user     system      total        real
fib1.rb: if        6158.150000   3.740000 6161.890000 (6167.126104)
fib2.rb: case      5702.300000   5.630000 5707.930000 (5722.498983)
fib3.rb: simple if 4837.140000   3.320000 4840.460000 (4846.447905)
fib4.rb: simple ?: 4969.330000   5.540000 4974.870000 (4986.140834)
fib6.rb: Hash        0.020000   0.000000   0.020000 (  0.021521)
fib7.rb: formula     0.240000   0.000000   0.240000 (  0.245543)
fib8.rb: BigMath    27.740000   0.040000  27.780000 ( 27.828623)