Skip to content

Commit 4926055

Browse files
committed
Updated Fibonacci algorithm
1 parent a5aed92 commit 4926055

File tree

1 file changed

+42
-321
lines changed

1 file changed

+42
-321
lines changed

maths/fibonacci.py

+42-321
Original file line numberDiff line numberDiff line change
@@ -1,332 +1,53 @@
1-
"""
2-
Calculates the Fibonacci sequence using iteration, recursion, memoization,
3-
and a simplified form of Binet's formula
4-
5-
NOTE 1: the iterative, recursive, memoization functions are more accurate than
6-
the Binet's formula function because the Binet formula function uses floats
7-
8-
NOTE 2: the Binet's formula function is much more limited in the size of inputs
9-
that it can handle due to the size limitations of Python floats
10-
NOTE 3: the matrix function is the fastest and most memory efficient for large n
11-
12-
13-
See benchmark numbers in __main__ for performance comparisons/
14-
https://en.wikipedia.org/wiki/Fibonacci_number for more information
15-
"""
16-
17-
import functools
18-
from collections.abc import Iterator
19-
from math import sqrt
20-
from time import time
21-
22-
import numpy as np
23-
from numpy import ndarray
24-
25-
26-
def time_func(func, *args, **kwargs):
27-
"""
28-
Times the execution of a function with parameters
29-
"""
30-
start = time()
31-
output = func(*args, **kwargs)
32-
end = time()
33-
if int(end - start) > 0:
34-
print(f"{func.__name__} runtime: {(end - start):0.4f} s")
35-
else:
36-
print(f"{func.__name__} runtime: {(end - start) * 1000:0.4f} ms")
37-
return output
38-
39-
40-
def fib_iterative_yield(n: int) -> Iterator[int]:
41-
"""
42-
Calculates the first n (1-indexed) Fibonacci numbers using iteration with yield
43-
>>> list(fib_iterative_yield(0))
44-
[0]
45-
>>> tuple(fib_iterative_yield(1))
46-
(0, 1)
47-
>>> tuple(fib_iterative_yield(5))
48-
(0, 1, 1, 2, 3, 5)
49-
>>> tuple(fib_iterative_yield(10))
50-
(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55)
51-
>>> tuple(fib_iterative_yield(-1))
52-
Traceback (most recent call last):
53-
...
54-
ValueError: n is negative
55-
"""
56-
if n < 0:
57-
raise ValueError("n is negative")
58-
a, b = 0, 1
59-
yield a
60-
for _ in range(n):
61-
yield b
62-
a, b = b, a + b
63-
64-
65-
def fib_iterative(n: int) -> list[int]:
66-
"""
67-
Calculates the first n (0-indexed) Fibonacci numbers using iteration
68-
>>> fib_iterative(0)
69-
[0]
70-
>>> fib_iterative(1)
71-
[0, 1]
72-
>>> fib_iterative(5)
73-
[0, 1, 1, 2, 3, 5]
74-
>>> fib_iterative(10)
75-
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
76-
>>> fib_iterative(-1)
77-
Traceback (most recent call last):
78-
...
79-
ValueError: n is negative
1+
def fibonacci(n, method="iterative"):
802
"""
81-
if n < 0:
82-
raise ValueError("n is negative")
83-
if n == 0:
84-
return [0]
85-
fib = [0, 1]
86-
for _ in range(n - 1):
87-
fib.append(fib[-1] + fib[-2])
88-
return fib
89-
90-
91-
def fib_recursive(n: int) -> list[int]:
92-
"""
93-
Calculates the first n (0-indexed) Fibonacci numbers using recursion
94-
>>> fib_iterative(0)
95-
[0]
96-
>>> fib_iterative(1)
97-
[0, 1]
98-
>>> fib_iterative(5)
99-
[0, 1, 1, 2, 3, 5]
100-
>>> fib_iterative(10)
101-
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
102-
>>> fib_iterative(-1)
103-
Traceback (most recent call last):
104-
...
105-
ValueError: n is negative
106-
"""
107-
108-
def fib_recursive_term(i: int) -> int:
109-
"""
110-
Calculates the i-th (0-indexed) Fibonacci number using recursion
111-
>>> fib_recursive_term(0)
112-
0
113-
>>> fib_recursive_term(1)
114-
1
115-
>>> fib_recursive_term(5)
116-
5
117-
>>> fib_recursive_term(10)
118-
55
119-
>>> fib_recursive_term(-1)
120-
Traceback (most recent call last):
121-
...
122-
Exception: n is negative
123-
"""
124-
if i < 0:
125-
raise ValueError("n is negative")
126-
if i < 2:
127-
return i
128-
return fib_recursive_term(i - 1) + fib_recursive_term(i - 2)
129-
130-
if n < 0:
131-
raise ValueError("n is negative")
132-
return [fib_recursive_term(i) for i in range(n + 1)]
133-
134-
135-
def fib_recursive_cached(n: int) -> list[int]:
136-
"""
137-
Calculates the first n (0-indexed) Fibonacci numbers using recursion
138-
>>> fib_iterative(0)
139-
[0]
140-
>>> fib_iterative(1)
141-
[0, 1]
142-
>>> fib_iterative(5)
143-
[0, 1, 1, 2, 3, 5]
144-
>>> fib_iterative(10)
145-
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
146-
>>> fib_iterative(-1)
147-
Traceback (most recent call last):
148-
...
149-
ValueError: n is negative
150-
"""
151-
152-
@functools.cache
153-
def fib_recursive_term(i: int) -> int:
154-
"""
155-
Calculates the i-th (0-indexed) Fibonacci number using recursion
156-
"""
157-
if i < 0:
158-
raise ValueError("n is negative")
159-
if i < 2:
160-
return i
161-
return fib_recursive_term(i - 1) + fib_recursive_term(i - 2)
162-
163-
if n < 0:
164-
raise ValueError("n is negative")
165-
return [fib_recursive_term(i) for i in range(n + 1)]
3+
Compute the Fibonacci number using the specified method.
1664
167-
168-
def fib_memoization(n: int) -> list[int]:
169-
"""
170-
Calculates the first n (0-indexed) Fibonacci numbers using memoization
171-
>>> fib_memoization(0)
172-
[0]
173-
>>> fib_memoization(1)
174-
[0, 1]
175-
>>> fib_memoization(5)
176-
[0, 1, 1, 2, 3, 5]
177-
>>> fib_memoization(10)
178-
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
179-
>>> fib_iterative(-1)
180-
Traceback (most recent call last):
181-
...
182-
ValueError: n is negative
183-
"""
184-
if n < 0:
185-
raise ValueError("n is negative")
186-
# Cache must be outside recursuive function
187-
# other it will reset every time it calls itself.
188-
cache: dict[int, int] = {0: 0, 1: 1, 2: 1} # Prefilled cache
189-
190-
def rec_fn_memoized(num: int) -> int:
191-
if num in cache:
192-
return cache[num]
193-
194-
value = rec_fn_memoized(num - 1) + rec_fn_memoized(num - 2)
195-
cache[num] = value
196-
return value
197-
198-
return [rec_fn_memoized(i) for i in range(n + 1)]
199-
200-
201-
def fib_binet(n: int) -> list[int]:
202-
"""
203-
Calculates the first n (0-indexed) Fibonacci numbers using a simplified form
204-
of Binet's formula:
205-
https://en.m.wikipedia.org/wiki/Fibonacci_number#Computation_by_rounding
206-
207-
NOTE 1: this function diverges from fib_iterative at around n = 71, likely
208-
due to compounding floating-point arithmetic errors
209-
210-
NOTE 2: this function doesn't accept n >= 1475 because it overflows
211-
thereafter due to the size limitations of Python floats
212-
>>> fib_binet(0)
213-
[0]
214-
>>> fib_binet(1)
215-
[0, 1]
216-
>>> fib_binet(5)
217-
[0, 1, 1, 2, 3, 5]
218-
>>> fib_binet(10)
219-
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
220-
>>> fib_binet(-1)
221-
Traceback (most recent call last):
222-
...
223-
ValueError: n is negative
224-
>>> fib_binet(1475)
225-
Traceback (most recent call last):
226-
...
227-
ValueError: n is too large
228-
"""
229-
if n < 0:
230-
raise ValueError("n is negative")
231-
if n >= 1475:
232-
raise ValueError("n is too large")
233-
sqrt_5 = sqrt(5)
234-
phi = (1 + sqrt_5) / 2
235-
return [round(phi**i / sqrt_5) for i in range(n + 1)]
236-
237-
238-
def matrix_pow_np(m: ndarray, power: int) -> ndarray:
239-
"""
240-
Raises a matrix to the power of 'power' using binary exponentiation.
241-
242-
Args:
243-
m: Matrix as a numpy array.
244-
power: The power to which the matrix is to be raised.
5+
Parameters:
6+
- n (int): The nth Fibonacci number to calculate.
7+
- method (str): The method to use ("iterative", "recursive", "memoized").
2458
2469
Returns:
247-
The matrix raised to the power.
248-
249-
Raises:
250-
ValueError: If power is negative.
251-
252-
>>> m = np.array([[1, 1], [1, 0]], dtype=int)
253-
>>> matrix_pow_np(m, 0) # Identity matrix when raised to the power of 0
254-
array([[1, 0],
255-
[0, 1]])
256-
257-
>>> matrix_pow_np(m, 1) # Same matrix when raised to the power of 1
258-
array([[1, 1],
259-
[1, 0]])
260-
261-
>>> matrix_pow_np(m, 5)
262-
array([[8, 5],
263-
[5, 3]])
264-
265-
>>> matrix_pow_np(m, -1)
266-
Traceback (most recent call last):
267-
...
268-
ValueError: power is negative
269-
"""
270-
result = np.array([[1, 0], [0, 1]], dtype=int) # Identity Matrix
271-
base = m
272-
if power < 0: # Negative power is not allowed
273-
raise ValueError("power is negative")
274-
while power:
275-
if power % 2 == 1:
276-
result = np.dot(result, base)
277-
base = np.dot(base, base)
278-
power //= 2
279-
return result
280-
281-
282-
def fib_matrix_np(n: int) -> int:
10+
- int: The nth Fibonacci number.
28311
"""
284-
Calculates the n-th Fibonacci number using matrix exponentiation.
285-
https://www.nayuki.io/page/fast-fibonacci-algorithms#:~:text=
286-
Summary:%20The%20two%20fast%20Fibonacci%20algorithms%20are%20matrix
287-
288-
Args:
289-
n: Fibonacci sequence index
290-
291-
Returns:
292-
The n-th Fibonacci number.
29312

294-
Raises:
295-
ValueError: If n is negative.
296-
297-
>>> fib_matrix_np(0)
298-
0
299-
>>> fib_matrix_np(1)
300-
1
301-
>>> fib_matrix_np(5)
302-
5
303-
>>> fib_matrix_np(10)
304-
55
305-
>>> fib_matrix_np(-1)
306-
Traceback (most recent call last):
307-
...
308-
ValueError: n is negative
309-
"""
31013
if n < 0:
311-
raise ValueError("n is negative")
312-
if n == 0:
313-
return 0
14+
raise ValueError("Input must be a non-negative integer.")
15+
16+
# Iterative Approach (Default)
17+
if method == "iterative":
18+
a, b = 0, 1
19+
for _ in range(n):
20+
a, b = b, a + b
21+
return a
22+
23+
# Recursive Approach
24+
elif method == "recursive":
25+
if n == 0:
26+
return 0
27+
elif n == 1:
28+
return 1
29+
return fibonacci(n - 1, "recursive") + fibonacci(n - 2, "recursive")
30+
31+
# Memoized Approach
32+
elif method == "memoized":
33+
memo = {}
34+
35+
def fib_memo(n):
36+
if n in memo:
37+
return memo[n]
38+
if n <= 1:
39+
return n
40+
memo[n] = fib_memo(n - 1) + fib_memo(n - 2)
41+
return memo[n]
42+
43+
return fib_memo(n)
31444

315-
m = np.array([[1, 1], [1, 0]], dtype=int)
316-
result = matrix_pow_np(m, n - 1)
317-
return int(result[0, 0])
45+
else:
46+
raise ValueError("Invalid method. Choose 'iterative', 'recursive', or 'memoized'.")
31847

31948

49+
# Example Usage:
32050
if __name__ == "__main__":
321-
from doctest import testmod
322-
323-
testmod()
324-
# Time on an M1 MacBook Pro -- Fastest to slowest
325-
num = 30
326-
time_func(fib_iterative_yield, num) # 0.0012 ms
327-
time_func(fib_iterative, num) # 0.0031 ms
328-
time_func(fib_binet, num) # 0.0062 ms
329-
time_func(fib_memoization, num) # 0.0100 ms
330-
time_func(fib_recursive_cached, num) # 0.0153 ms
331-
time_func(fib_recursive, num) # 257.0910 ms
332-
time_func(fib_matrix_np, num) # 0.0000 ms
51+
print(fibonacci(10)) # Default (iterative)
52+
print(fibonacci(10, "recursive")) # Recursive method
53+
print(fibonacci(10, "memoized")) # Memoized method

0 commit comments

Comments
 (0)