What recursion is
A function is recursive when it calls itself. The idea is to solve a problem by solving a smaller copy of the same problem and then using that answer to finish the original one. Recursion fits any problem that has this shape: factorials, searching a sorted array, walking through nested structures such as folders or trees, and many more.
Every correct recursive function has two parts:
- a base case, an input small enough to answer directly, without another call;
- a recursive case, which calls the function on a smaller input and builds the answer from the result.
Without a base case the calls never stop. Each call waits on the call stack for the next one to finish, so the stack grows until the program fails.
The exercise: factorial
The factorial of a non-negative integer n, written n!, is 1 * 2 * 3 * ... * n. The function factorial takes an Int n and returns n!. For n = 5 the result is 120, because 1 * 2 * 3 * 4 * 5 = 120.
The statement allows O(n) time and O(n) extra space. The extra space is the call stack, which holds one pending call per level of recursion.
The key observation is that n! = n * (n - 1)!. A factorial is just n times a smaller factorial. The smallest case is 0! = 1, and that is the base case.
Step by step
- Write the base case. If
n == 0, return 1. This is the input that can be answered directly, and it is also what stops the recursion. - Write the recursive case. Return
factorial(n - 1) * n. The argumentn - 1is smaller thann, so every call is closer to the base case.
The solution in Kotlin
class Solution {
fun factorial(n: Int): Int {
if (n == 0) {
return 1
}
return factorial(n - 1) * n
}
}
Tracing the calls
For factorial(3), the calls go down and the answers come back up:
factorial(3)
= factorial(2) * 3
= (factorial(1) * 2) * 3
= ((factorial(0) * 1) * 2) * 3
= ((1 * 1) * 2) * 3
= 6
Nothing is multiplied until factorial(0) returns 1. After that the pending multiplications run from the inside out, as each call returns to the one that made it. At the deepest point there are n + 1 calls waiting at once, which is why the space is O(n). Each call does a constant amount of work, so the time is O(n).
Kotlin details and gotchas
- Return type is required. A function with a block body and no declared return type returns
Unit. A recursive function should state its type, here: Int. For an expression body, Kotlin also requires the type when the function calls itself:fun factorial(n: Int): Int = if (n == 0) 1 else n * factorial(n - 1)is the same function written on one line. Intoverflows silently. AnIntholds up to 2147483647, and 12! = 479001600 is the largest factorial that fits.factorial(13)returns 1932053504, which is not 13!, andfactorial(20)returns -2102132736. No exception is thrown. WithLong, the largest factorial that fits is 20! = 2432902008176640000; 21! wraps around to -4249290049419214848.- A missing base case or a wrong direction.
factorial(-1)never reaches 0, because it callsfactorial(-2),factorial(-3)and so on. Eventually the JVM throwsStackOverflowError. The problem promisesn >= 0, so the lesson doesn't guard for it. - Parentheses in the call.
factorial(n - 1)subtracts first and passes the result.factorial(n) - 1would call the function with the samenagain and recurse forever. - Recursion versus a loop. A
forloop computes the same product withO(1)extra space. This lesson uses recursion to practise the technique, and the other recursive problems in the chapter reuse the same two-part structure.
Common mistakes
- Returning 0 from the base case. Then every product ends up 0.
- Writing the base case as
n == 1only. It works forn >= 1but recurses until the stack overflows forn = 0, which the problem allows. - Forgetting
returnin front of the recursive call, so the value is computed and thrown away. The compiler reports that a function with a block body needs areturn.
Testing
factorial(0)gives 1.factorial(1)gives 1.factorial(5)gives 120.factorial(12)gives 479001600.
Key takeaways
- A recursive function has a base case that answers directly and a recursive case that works on a smaller input.
- Each call waits on the call stack, so recursion depth costs memory.
- In Kotlin, an
Intresult overflows silently beyond 12!; useLongfor larger inputs.