Problem overview
The function fibonacci(n: Int) returns the n-th Fibonacci number. The sequence starts with F(0) = 0 and F(1) = 1, and every later number is the sum of the two before it: F(n) = F(n - 1) + F(n - 2). The sequence begins 0, 1, 1, 2, 3, 5, 8, 13, 21.
For n = 4 the answer is 3, since F(4) = F(3) + F(2) = 2 + 1. The statement asks for O(n) time and O(1) space.
Approach
The definition suggests a recursive function that calls itself for n - 1 and n - 2. That works, but it recomputes the same values again and again, and the number of calls grows exponentially. The sequence can be computed by building it from the bottom instead.
To get the next number you only need the previous two, so there is no reason to remember the rest. Keep two variables, prev and curr, holding F(i - 2) and F(i - 1). On each pass, compute F(i) as their sum and slide both variables one step forward. After the last pass, curr holds F(n). That is one loop and two variables.
Step by step
- Handle the base cases. If
n < 2, returnn. This works for both:F(0) = 0andF(1) = 1, so the answer equals the input. - Set up the window.
var prev = 0andvar curr = 1holdF(0)andF(1). They arevarbecause they change on every pass. - Loop from 2 to
n.for (i in 2..n)runs once for each ofF(2)toF(n). The..range includesn. - Compute the new number.
val next = curr + previsF(i). It is avalbecause it is created fresh on every pass and never changed. - Slide the window. Assign
prev = currfirst, thencurr = next. The order protects the oldcurr: ifcurrwere overwritten first, its value would be lost beforeprevcould take it. - Return
curr. After the final pass it holdsF(n).
Tracing n = 4: start with prev = 0, curr = 1. At i = 2, next = 1, so prev = 1 and curr = 1. At i = 3, next = 2, so prev = 1 and curr = 2. At i = 4, next = 3, so prev = 2 and curr = 3. The function returns 3.
Why the loop is correct
A useful way to check a loop is to state what is true at the start of every pass. Here, at the start of the pass for i, prev holds F(i - 2) and curr holds F(i - 1). Before the first pass (i = 2) that is true, because prev = F(0) and curr = F(1). The pass computes F(i) and moves the window so the same statement holds for i + 1. When the loop ends after i = n, curr holds F(n). If a change you make breaks this statement, the answer will be wrong.
The solution in Kotlin
class Solution {
fun fibonacci(n: Int): Int {
if (n < 2) {
return n
}
var prev = 0
var curr = 1
for (i in 2..n) {
val next = curr + prev
prev = curr
curr = next
}
return curr
}
}
Kotlin details:
- Inclusive range.
2..ncontains both 2 andn. Whennis 2 it runs once. Because the base case already returned forn < 2, the range is never empty here. valinside a loop. A newnextis created in each pass, so it can be read-only.Intoverflow. AnIntholds values up to 2147483647 and wraps silently past that.fibonacci(46)returns1836311903, the largest Fibonacci number that fits.fibonacci(47)returns-1323752223, which is wrong. For largern, declareprevandcurrasLong, which holds values up toF(92)(7540113804746346429); withLongvariables,F(47)comes out as2971215073.- Not a swap. Kotlin has no tuple assignment like
prev, curr = curr, prev + curr, so the three-step update withnextis the straightforward way. The values come out the same.
The window idea is also what makes the space constant. Older numbers such as F(i - 3) are never needed again, so the variables simply move past them and the program forgets them.
Complexity
Time: O(n). The loop runs n - 1 times and does a constant amount of work each time.
Space: O(1). Only prev, curr, next and the loop counter exist, regardless of n. A recursive version would use O(n) stack space, and a version that stores every number in a list would use O(n) memory.
Why not plain recursion
The recursive version follows the definition directly:
fun fibRec(n: Int): Int = if (n < 2) n else fibRec(n - 1) + fibRec(n - 2)
It returns the right values, but each call starts two more, and the same arguments are computed many times. Counting the calls shows the cost: fibRec(10) makes 177 calls, fibRec(20) makes 21,891 and fibRec(30) makes 2,692,537. The count grows by a factor of about 1.6 for each increase of 1 in n, which is exponential. The loop computes each number once, so for n = 30 it does 29 passes instead of millions of calls.
Edge cases and common mistakes
n = 0andn = 1. Without the base case the loop2..nis empty, and the function would returncurr, which is 1. That's right forn = 1but wrong forn = 0, whose answer is 0.- Updating in the wrong order. Writing
curr = nextbeforeprev = currmakes both variables equalnext, so the sequence goes wrong from that step on. - Returning
prev. After the final pass,F(n)is incurr;prevholdsF(n - 1). - Starting the loop at 3 or stopping at
n - 1. Either one performs one pass too few and returnsF(n - 1). - Overflow. Past
n = 46, anIntresult is wrong because of wrap-around.
Testing
n = 0gives0andn = 1gives1.n = 2gives1.n = 3gives2.n = 4gives3.n = 10gives55.