Fibonacci Number
Imports
# python standard library
from datetime import (
datetime,
timedelta,
)
import random
# third party
from joblib import Memory
The Fibonacci Problem
THe Fibonacci Sequence is defined as
\[F_0 = 0, F_1 = 1,\ldots, F_i =F_{i-1} + F_{i-2} \text{ for } i \ge 2\]
Description | |
---|---|
Task | Given an integer n, find the /n/th Fibonacci number \(F_n\). |
Input | A single integer n. |
Constraints | \(0 \le n \le 45\) |
Output | \(F_n\) |
Sample Values
Input | Output | Meaning |
---|---|---|
10 | 55 | \(F_10=55\) |
Constants
These are translations from the problem description
MAX_TIME = timedelta(seconds=5)
MAX_INPUT = 45
Helpers
def time_it(implementation, tag, input_value, expected, max_time=MAX_TIME):
"""Times the implementation
Args:
implementation: callable to pass input_value to
tag (str): identifier to add to the output
input_value (int): the number to pass to the implementation
expected (int): the expected value
max_time (float): number of seconds allowed
Raises:
AssertionError: wrong value or took too long
"""
assert input_value <= MAX_INPUT, "n too large: {}, max allowed: {}".format(input_value,
MAX_INPUT)
start = datetime.now()
print("Starting {}".format(tag))
actual = implementation(input_value)
assert actual == expected, "Actual: {} Expected: {}".format(
actual, expected)
elapsed = datetime.now() - start
print("({}) Okay Elapsed time: {}".format(tag, elapsed))
assert elapsed <= max_time, "Time Greater than {}".format(max_time)
return
Naive Implementation
The 'naive' implementation uses recursion to calculate a fibonacci number.
def calculate_fibonacci_recursive(n):
"""Calculates the nth fibonacci number
Args:
n (int): the fibonacci number to get (e.g. 3 means third)
Returns:
int: nth fibonacci number
"""
if (n <= 1):
return n
return (calculate_fibonacci_recursive(n - 1)
+ calculate_fibonacci_recursive(n - 2))
time_it(calculate_fibonacci_recursive, "Recursive", 10, 55)
This fails the grader (as expected).
Failed case #37/46: time limit exceeded Input: 36 Your output: 14930352 stderr: (Time used: 5.63/5.00, memory used: 9613312/536870912.)
The Tester
We have kind of a chicken and the egg problem here. We know that the recursive version is correct, but it is too slow. But in order to validate our newer versions, we need to run it to check for correctness. To solve this problem I'm going to use a cache.
memory = Memory(location="memories")
@memory.cache
def reference_implementation(n):
"""Calculates the nth fibonacci number
Args:
n (int): the fibonacci number to get (e.g. 3 means third)
Returns:
int: nth fibonacci number
"""
if (n <= 1):
return n
return (calculate_fibonacci_recursive(n - 1)
+ calculate_fibonacci_recursive(n - 2))
def time_once(implementation, n):
"""Runs the implementation once
Args:
implementation: callable to pass input
n: value to pass to the implementation
Returns:
output of implementation
"""
start = datetime.now()
output = implementation(n)
print("Elapsed: {}".format(datetime.now() - start))
return output
def run_range(n):
"""run the reference implementation n times
Args:
n (int): number of times to run the reference implementation
"""
start = datetime.now()
for input_value in range(n):
output = reference_implementation(input_value)
return
for endpoint in range(0, MAX_INPUT + 1, 10):
print("endpoint: {}".format(endpoint))
for n in range(endpoint):
run_range(n)
time_once(reference_implementation, 45)
In case I accidentally re-run that last call and it uses the cache I'll note here that the original run time was 8 minutes and 40 seconds.
class Tester:
"""Class to test the implementation
Args:
implementation: callable to pass input_value to
tag (str): identifier to add to the output
iterations (int): number of times to run the testing
verbose (bool): if true, emit more text
max_time (float): number of seconds allowed
"""
def __init__(self, implementation, tag, iterations,
verbose=False,
max_time=MAX_TIME):
self.implementation = implementation
self.tag = tag
self.max_time = max_time
self.verbose = verbose
self.iterations = iterations
return
def output(self, statement):
"""prints the statement if verbose is on"""
if self.verbose:
print(statement)
def time_it(self, input_value):
"""Times the implementation
.. warning:: This uses the ``reference_implementation`` to get the
expected value. Make sure it's implemented and the values are cached
Args:
input_value (int): input for the implementation
Raises:
AssertionError: wrong value or took too long
"""
start = datetime.now()
self.output("Starting {}".format(self.tag))
expected = reference_implementation(input_value)
actual = self.implementation(input_value)
assert actual == expected, "n: {} Actual: {} Expected: {}".format(
input_value, actual, expected)
elapsed = datetime.now() - start
self.output("({}) Okay Elapsed time: {}".format(self.tag, elapsed))
assert elapsed <= self.max_time, "Time Greater than {}".format(self.max_time)
return
def __call__(self):
"""Generates random numbers and times it"""
start = datetime.now()
print("***** {} *****".format(self.tag))
for iteration in range(self.iterations):
n = random.randrange(MAX_INPUT + 1)
self.output("n: {}".format(n))
self.time_it(n)
print("Total Elapsed: {}".format(datetime.now()))
return
An Iterative Version
To try and speed things up I'm going to use an iterative version instead of a recursive one.
def fibonacci_iterative(n):
"""Calculates the nth fibonacci number
Args:
n (int): the fibonacci number to get (e.g. 3 means third)
Returns:
int: nth fibonacci number
"""
first = (0, 1)
if n in first:
return n
previous, current = first
for index in range(2, n + 1):
previous, current = current, previous + current
return current
test = Tester(fibonacci_iterative, "Iterative", 1000)
test()
f_0 = fibonacci_iterative(45)
f_1 = reference_implementation(45)
print(f_0)
print(f_1)
assert f_0 == f_1
This passes the grader.
Good job! (Max time used: 0.03/5.00, max memory used: 9637888/536870912.)