You can also find all 35 answers here π Devinterview.io - Fibonacci Sequence
The Fibonacci Sequence is a series of numbers where each number
with initial conditions
The ratio of consecutive Fibonacci numbers approximates the Golden Ratio (
The sequence frequently manifests in nature, such as in flower petals, seedhead spirals, and seashell growth patterns.
Here is the Python code:
# Using recursion
def fibonacci_recursive(n):
if n <= 0: return 0
elif n == 1: return 1
return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
# Using dynamic programming for efficiency
def fibonacci_dynamic(n):
fib = [0, 1]
for i in range(2, n+1):
fib.append(fib[-1] + fib[-2])
return fib[n]
The task is to write a function that returns the $n$th Fibonacci number using a recursive approach.
The naive recursive solution for the Fibonacci series, while easy to understand, is inefficient due to its exponential time complexity of
Dynamic Programming methods like memoization and tabulation result in optimized time complexity.
- Check for the base cases, i.e., if
$n$ is 0 or 1. - If not a base case, recursively compute
$F(n-1)$ and$F(n-2)$ . - Return the sum of the two recursive calls.
Here is the Python code:
def fib_recursive(n):
if n <= 1:
return n
return fib_recursive(n-1) + fib_recursive(n-2)
-
Time Complexity:
$O(2^n)$ . This is due to the two recursive calls made at each level of the recursion tree, resulting in an exponential number of function calls. -
Space Complexity:
$O(n)$ . Despite the inefficient time complexity, the space complexity is$O(n)$ as it represents the depth of the recursion stack.
The task is to generate the Fibonacci sequence to the $n$th number using a non-recursive approach.
While recursion offers a straightforward solution for the Fibonacci sequence, it has performance and stack overflow issues. A non-recursive approach, often based on a loop or iterative method, overcomes these limitations.
Here, I'll present both binet's formula and an iterative method as the non-recursive solutions.
where:
-
$\phi = \frac{{1 + \sqrt 5}}{2}$ (the golden ratio) $\psi = \frac{{1 - \sqrt 5}}{2}$
- Compute
$\phi$ and$\psi$ . - Plug values into the formula for
$F(n)$ .
-
Time Complexity:
$O(1)$ -
Space Complexity:
$O(1)$
Note: While Binet's formula offers an elegant non-recursive solution, it's sensitive to floating-point errors which can impact accuracy, especially for large
- Initialize
$\text{prev} = 0$ and$\text{curr} = 1$ . These are the first two Fibonacci numbers. - For
$i = 2$ to$n$ , update$\text{prev}$ and$\text{curr}$ to be$\text{prev} + \text{curr}$ and$\text{prev}$ respectively. These become the next numbers in the sequence.
-
Time Complexity:
$O(n)$ -
Space Complexity:
$O(1)$
Here's the Python code for both methods:
import math
def fib_binet(n):
phi = (1 + math.sqrt(5)) / 2
psi = (1 - math.sqrt(5)) / 2
return int((phi**n - psi**n) / math.sqrt(5))
# Output
print(fib_binet(5)) # Output: 5
def fib_iterative(n):
if n <= 1:
return n
prev, curr = 0, 1
for _ in range(2, n + 1):
prev, curr = curr, prev + curr
return curr
# Output
print(fib_iterative(5)) # Output: 5
Both functions will return the 5th Fibonacci number.
The naive recursive implementation of the Fibonacci sequence has a time complexity of
This is the straightforward, but inefficient, method.
-
Base Case: Return 0 if
$n = 0$ and 1 if$n = 1$ . -
Function Call: Recur on the sum of the
$n-1$ and$n-2$ elements.
Here is the Python code:
def fibonacci_recursive(n):
if n <= 1:
return n
return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
-
Time Complexity:
$O(2^n)$ - As each call branches into two more calls (with the exception of the base case), the number of function calls grows exponentially with$n$ , resulting in this time complexity. -
Space Complexity:
$O(n)$ - The depth of the recursion stack can go up to$n$ due to the$n-1$ and$n-2$ calls.
Using memoization allows for a noticeable performance boost.
- Initialize a cache,
fib_cache
, with default values of -1. -
Base Case: If the $n$th value is already calculated (i.e.,
fib_cache[n] != -1
), return that value. Otherwise, calculate the $n$th value using recursion. -
Cache the Result: Once the $n$th value is determined, store it in
fib_cache
before returning.
Here is the Python code:
def fibonacci_memo(n, fib_cache={0: 0, 1: 1}):
if n not in fib_cache:
fib_cache[n] = fibonacci_memo(n-1, fib_cache) + fibonacci_memo(n-2, fib_cache)
return fib_cache[n]
-
Time Complexity:
$O(n)$ - Each$n$ is computed once and then stored in the cache, so subsequent computations are$O(1)$ . -
Space Complexity:
$O(n)$ - The space used by the cache.
The iterative approach shines in terms of time and space efficiency.
- Initialize
a
andb
as 0 and 1, respectively. -
Loop: Update
a
andb
to the next two Fibonacci numbers, replacing them as necessary to ensure they represent the desired numbers. -
Return:
a
after the loop exits, since it stores the $n$th Fibonacci number.
Here is the Python code:
def fibonacci_iterative(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
-
Time Complexity:
$O(n)$ - The function iterates a constant number of times, depending on$n$ . -
Space Complexity:
$O(1)$ - The variablesa
andb
are updated in place without utilizing any dynamic data structures or recursion stacks.
Memoization is a technique that makes dynamic programming faster by storing the results of expensive function calls and reusing them.
To apply memoization to the Fibonacci sequence, a list or dictionary (array or hashmap in terms of computer science) is used to store the intermediate results, essentially turning the calculation process into a more optimized dynamic programming algorithm.
Here is the Python code:
def fibonacci_memo(n, memo={0: 0, 1: 1}):
if n not in memo:
memo[n] = fibonacci_memo(n-1, memo) + fibonacci_memo(n-2, memo)
return memo[n]
# Test
print(fibonacci_memo(10)) # Outputs: 55
6. Implement an iterative solution to generate the Fibonacci sequence, discussing its time and space complexity.
The task is to generate the Fibonacci sequence using an iterative approach, and then analyzing its time and space complexity.
- Initialize
a = 0
andb = 1
. - Use a loop to update
a
andb
. On each iteration:- Update
a
to the value ofb
. - Update
b
to the sum of its old value and the old value ofa
.
- Update
- Repeat the loop 'n-1' times, where 'n' is the desired sequence length.
Here's how the first few Fibonacci numbers are computed:
- Iteration 1:
$a = 0, , b = 1, , b = 0 + 1 = 1$ - Iteration 2:
$a = 1, , b = 1, , b = 1 + 1 = 2$ - Iteration 3:
$a = 1, , b = 2, , b = 2 + 1 = 3$ - Iteration 4:
$a = 2, , b = 3, , b = 3 + 2 = 5$ - And so on...
-
Time Complexity:
$O(n)$ β This is more efficient than the recursive approach which is$O(2^n)$ . -
Space Complexity:
$O(1)$ β The space used is constant, regardless of the input 'n'.
Here is the Python code:
def fibonacci_iterative(n):
if n <= 0:
return "Invalid input. n must be a positive integer."
fib_sequence = [0] if n >= 1 else []
a, b = 0, 1
for _ in range(2, n+1):
fib_sequence.append(b)
a, b = b, a + b
return fib_sequence
# Example usage
print(fibonacci_iterative(10)) # Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Dynamic Programming is a powerful algorithmic technique that is widely used to optimize recursive problems, like computing the Fibonacci sequence, by avoiding redundant computations.
The naive method of Fibonacci computation is highly inefficient, often taking exponential time. Dynamic Programming offers better time complexity, often linear or even constant, without sacrificing accuracy.
The most common way of optimizing Fibonacci computations is through memoization, where function calls are stored with their results for future reference. In Python, you can employ decorators or dictionaries to achieve this.
Here is the Python code:
def memoize(fib):
cache = {}
def wrapper(n):
if n not in cache:
cache[n] = fib(n)
return cache[n]
return wrapper
@memoize
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
# Test
print(fib(10)) # 55
The task is to compute the $n$th Fibonacci number using matrix exponentiation and analyze its efficiency.
Matrix exponentiation offers an optimal
- Represent the Fibonacci transformation as $F = \begin{bmatrix} 1 & 1 \ 1 & 0 \end{bmatrix}$.
- Utilize exponentiation by squaring to efficiently compute
$F^n$ for large$n$ . - Extract the
$n$ -th Fibonacci number as the top right element of the resultant matrix.
-
Time Complexity:
$O(\log n)$ due to the efficiency of exponentiation by squaring. -
Space Complexity:
$O(\log n)$
Here is the Python code:
import numpy as np
def matrix_power(A, n):
if n == 1:
return A
if n % 2 == 0:
half = matrix_power(A, n // 2)
return np.dot(half, half)
else:
half = matrix_power(A, (n - 1) // 2)
return np.dot(np.dot(half, half), A)
def fibonacci(n):
if n <= 0:
return 0
F = np.array([[1, 1], [1, 0]])
result = matrix_power(F, n - 1)
return result[0, 0]
# Example usage
print(fibonacci(6)) # Output: 8
9. Can the nth Fibonacci number be found using the golden ratio, and if so, how would you implement this method?
The
Here is the Python code:
import math
def approximate_fibonacci(n):
golden_ratio = (1 + math.sqrt(5)) / 2
return round(golden_ratio ** n / math.sqrt(5))
# Test
print(approximate_fibonacci(10)) # Output: 55
10. Present an approach to precomputing Fibonacci numbers to answer multiple nth Fibonacci queries efficiently.
The objective is to compute
A precomputation strategy is suitable for scenarios where Fibonacci
numbers are repeatedly requested over a fixed range.
- Generate and store Fibonacci numbers from 0 to the highest
$n$ using an array or dictionary. This operation has a time complexity of$O(n)$ . - Subsequent queries are answered directly from the precomputed table with a time complexity of
$O(1)$ .
- Precomputation:
$O(\text{max_n})$ - Query time:
$O(1)$
Let's consider Python as our programming language.
Here is a Python function that precomputes Fibonacci numbers up to a certain limit and then returns the $n$th Fibonacci number based on the precomputed table.
def precompute_fibonacci(max_n):
fib = [0, 1]
a, b = 0, 1
while b < max_n:
a, b = b, a + b
fib.append(b)
return fib
def fibonacci(n, fib_table):
return fib_table[n] if n < len(fib_table) else -1
# Usage
max_n = 100
fib_table = precompute_fibonacci(max_n)
print(fibonacci(10, fib_table)) # 55
11. How might the Fibonacci sequence be altered to start with two arbitrary initial values? Provide an algorithm for such a sequence.
Modifying the Fibonacci sequence to start with arbitrary initial values still leads to a unique sequence.
The iterative approach can handle custom starting values and compute any term in the sequence through the specified algorithm.
- Input: Start values
a
andb
(with a not equal to b) and target termn
. - Check if
n
is 1 or 2. If it is, return the corresponding start value. - Otherwise, execute a loop
n-2
times and update the start values. - Compute the
n
-th term once the loop concludes.
Here is the Python code:
def custom_fibonacci(a, b, n):
if n == 1:
return a
elif n == 2:
return b
for _ in range(n-2):
a, b = b, a+b
return b
# Example with start values 3 and 4 for the 6th term
result = custom_fibonacci(3, 4, 6) # Output: 10
12. Explain an algorithm to compute the sum of the first n Fibonacci numbers without generating the entire sequence.
The Fibonacci sequence is a classic mathematical series defined by the following:
-
Sum of First n Numbers: The sum of the first
$n$ Fibonacci numbers is equal to$F(n+2) - 1$ .
This can be computed using a simple, iterative function:
-
n-th Fibonacci Number: It can be calculated using two seed values
$F(0)$ and$F(1)$ .
The general form is:
\begin{bmatrix} 1 & 1 \ 1 & 0 \ \end{bmatrix}^{(n-1)} \begin{bmatrix} F(1) \ F(0) \ \end{bmatrix} $$
Where:
-
$A$ and$B$ vary based on the initial seed values. - The matrix is multiplied by itself
$(n-1)$ times.
Here is the Python code:
def fibonacci_sum(n):
fib_curr, fib_next, fib_sum = 0, 1, 0
for _ in range(n):
fib_sum += fib_curr
fib_curr, fib_next = fib_next, fib_curr + fib_next
return fib_sum
The time complexity is
The Lucas Sequence, a variant of the Fibonacci sequence, starts with 2 and 1, rather than 0 and 1, and follows the same recursive structure as the classic sequence:
Compared to the Fibonacci sequence, the Lucas sequence offers:
-
Simpler Recurrence Relation: The Lucas sequence uses only addition for its recursive relation, which can be computationally more efficient than Fibonacci's addition and subtraction.
-
Alternate Closed-Form Expression: While the closed-form formula for the $n$th term of the Fibonacci sequence involves radicals, the Lucas sequence provides an alternate expression that can be easier to work with.
14. How can the Fibonacci sequence be used to solve the tiling problem, where you need to cover a 2xn rectangle with 2x1 tiles?
The Fibonacci sequence is closely related to the problem of covering a 2xN rectangle with 2x1 tiles, often referred to as the "tiling problem". It in fact offers a direct solution to this problem.
Covering a 2xN rectangle
This is a recursive relationship similar to the one used to define the Fibonacci numbers, making it clear that there is a connection between the two.
Here is the Python code:
def tiling_ways(n):
a, b = 1, 1
for _ in range(n):
a, b = b, a + b
return a
n = 5
ways_to_tile = tiling_ways(n)
print(f'Number of ways to tile a 2x{n} rectangle: {ways_to_tile}')
In this example, a, b = b, a + b
is a compact way to update a
and b
. It relies on the fact that the right-hand side of the assignment is evaluated first before being assigned to the left-hand side.
This approach has a time complexity of
Fibonacci numbers grow at a rapid rate, which can lead to integer overflow. Numerous strategies exist to circumvent this issue.
-
Using a Data Type with Larger Capacity: The
long long int
data type in C/C++ provides up to 19 digits of precision, thus accommodating Fibonacci numbers up to$F(92)$ . -
Using Built-in Arbitrary Precision Libraries: Certain programming languages such as Python and Ruby come with arbitrary-precision arithmetic support, making them suited for such computations.
-
Implementing Custom Arithmetic: Libraries like GMP (GNU Multiple Precision Arithmetic Library) and
bigInt
in Java enable the handling of arbitrary-precision operations. Additionally, rolling out a custom arithmetic procedure through arrays, linked lists, or strings is viable.
Here is the C++ code:
#include <iostream>
#include <vector>
using namespace std;
long long int fibonacci(int n) {
if (n <= 1) return n;
long long int a = 0, b = 1, c;
for (int i = 2; i <= n; ++i) {
c = a + b;
a = b;
b = c;
}
return b;
}
int main() {
int n = 100;
cout << "Fibonacci number at position " << n << " is: "
<< fibonacci(n) << endl;
return 0;
}