What recursion is
A function is recursive when it calls itself. It solves a problem by reducing it to a smaller version of the same problem, and keeps doing that until the problem is so small that the answer is known directly.
Every recursive function has two parts:
- the base case, an input for which the function returns an answer without calling itself;
- the recursive case, which calls the function on a smaller input and uses that result to build the answer.
Without a base case, or with a recursive case that doesn't move toward it, the calls never stop.
The exercise: factorial
The factorial of a non-negative integer n, written n!, is the product 1 * 2 * 3 * ... * n. For example 5! = 120. The function factorial(n int) int must compute it recursively, in O(n) time and O(n) extra space.
The recursive idea is in the definition: n! = n * (n-1)!. If you know the factorial of the number below n, one multiplication gives the factorial of n. The chain ends at 0! = 1, which is the base case.
Step by step
- Write the base case. If
n == 0, return 1. Zero factorial is 1 by definition, and it gives every chain of calls a place to stop. - Write the recursive case. Return
factorial(n-1) * n. The call asks the same function for the next smaller factorial, and the product withnturns it into the answer forn.
The solution in Go
func factorial(n int) int {
if n == 0 {
return 1
}
return factorial(n-1) * n
}
How the calls unfold
For factorial(3), each call waits for the next one:
factorial(3)
factorial(2) * 3
factorial(1) * 2
factorial(0) * 1
1
The base case returns 1. Then the waiting calls finish one after another, from the innermost out: factorial(1) is 1 * 1 = 1, factorial(2) is 1 * 2 = 2, and factorial(3) is 2 * 3 = 6. For factorial(5) the result is 120.
Each unfinished call is kept on the call stack until the one below it returns, so a call for n uses n + 1 stack frames at its deepest. That is why the space cost is O(n), even though the function declares no slice or map. The time is O(n): one call and one multiplication per value from n down to 0.
Go details
- Calling itself needs no special syntax. A top-level function can refer to its own name, as
factorialdoes inside its body. - Return types. The signature says the function returns an
int, and both paths return one. Go rejects a function whose end can be reached without a return. - No tail-call optimisation. The Go compiler doesn't turn recursion into a loop, so each call really uses a new stack frame. Go stacks grow dynamically and can get very large, so ordinary depths such as a few thousand are fine, but there is a limit.
- Integer overflow wraps silently.
intis 64 bits here.factorial(20)is 2432902008176640000, which still fits, butfactorial(21)overflows and gives -4249290049419214848, with no error. Usemath/bigfor larger factorials. - Loop alternative. A
forloop multiplying1by each number up tongives the same result withO(1)space. The recursive version is the point of this lesson because the same pattern, a base case plus a smaller call, carries over to trees, backtracking and divide-and-conquer.
Common mistakes
- Missing base case. Without the
n == 0check, the calls go on through -1, -2 and so on and never return. - Not getting closer to the base case. Calling
factorial(n)orfactorial(n+1)from inside never reaches 0. - Negative input. The problem promises
n >= 0. A negativenskips the base case forever, and the program eventually ends with a stack overflow, which is a fatal error in Go, not a panic that can be recovered. - Wrong base value. Returning 0 for
n == 0makes every result 0, since everything is multiplied by it. The base value must be 1, the neutral element of multiplication. - Forgetting to use the result. Calling
factorial(n-1)and not multiplying it into the return value throws the work away.
Key takeaways
- Recursion solves a problem by solving a smaller copy of it, until a base case answers directly.
- Always write the base case first, and make sure each call moves toward it.
- Recursive calls wait on the call stack, so depth
nmeansO(n)extra space. - Factorial grows fast: with a 64-bit
int, results above20!overflow.