What recursion is
A recursive function solves a problem by calling itself on a smaller version of the same problem. Every recursive function needs two parts:
- a base case, an input small enough to answer directly, with no further call;
- a recursive case, which reduces the input toward the base case and combines the result of the smaller call into the answer.
Without a base case the calls never stop. Without progress toward it, the same happens.
The exercise: factorial
The factorial of a non-negative integer n, written n!, is 1 * 2 * 3 * ... * n. So 5! = 120. The lesson asks for a recursive function factorial(n) with O(n) time and O(n) extra space.
The definition already contains the recursion: n! = n * (n - 1)!. The factorial of 5 is 5 times the factorial of 4, which is 4 times the factorial of 3, and so on down to 0! = 1, the base case. By convention 0! is 1, and the recursion needs that value to bottom out.
Step by step
- Handle the base case. If
n == 0, return 1 at once. - Handle the recursive case. Return
Self::factorial(n - 1) * nas the final expression, with noreturnand no semicolon. The call usesSelf::becausefactorialis an associated function ofSolution, with noselfparameter.
The solution in Rust
impl Solution {
fn factorial(n: i32) -> i32 {
if n == 0 {
return 1;
}
Self::factorial(n - 1) * n
}
}
How the calls unfold
For n = 3, the calls go down until the base case answers, and then each call finishes on the way back up:
factorial(3) = factorial(2) * 3
factorial(2) = factorial(1) * 2
factorial(1) = factorial(0) * 1
factorial(0) = 1
Now the multiplications happen from the bottom: factorial(1) is 1 * 1 = 1, factorial(2) is 1 * 2 = 2, and factorial(3) is 2 * 3 = 6.
All of these calls are alive at the same time while the deepest one runs, each holding its own n on the call stack. That explains the cost: there are n + 1 calls, each doing constant work, so the time is O(n), and the stack holds n + 1 frames at its deepest, so the extra space is O(n).
Rust details
- Early return and the final expression. The base case uses
return 1;to leave the function early. The last line has no semicolon, so its value is the function's result. Self::. Inside animplblock, a function withoutselfis called through the type, asSelf::factorial. Writing a barefactorial(...)does not compile.- Overflow. An
i32holds values up to 2,147,483,647, and 13! is larger than that. The lessons build with optimizations, where the multiplication wraps silently:factorial(12)returns479001600, butfactorial(13)returns1932053504, not 6227020800. A debug build would panic with an overflow error instead. The same function written withi64returns6227020800for 13. - No tail-call guarantee. Rust does not promise to turn recursion into a loop, so very deep recursion can use up the stack. Factorial overflows
i32long before that matters, but keep it in mind for other recursive functions.
Common mistakes
- Forgetting the base case. The calls never reach an answer, and the program keeps going until the stack runs out.
- Not moving toward the base case. Calling
Self::factorial(n)instead ofSelf::factorial(n - 1)repeats the same problem forever. - Negative input. The base case is exactly 0, so a negative
nnever reaches it by counting down. The problem promisesn >= 0. - Using 0 as the base answer. 0! is 1. Returning 0 would make every product 0.
- Writing
Self::factorial(n - 1) * n;with a semicolon. The function would then return()instead of ani32, and the compiler reports a mismatched type.
Testing
factorial(0)gives1.factorial(1)gives1.factorial(5)gives120.factorial(10)gives3628800.
Key takeaways
- A recursive function needs a base case and a recursive case that moves toward it.
- Each pending call occupies stack space, so recursion that goes
ndeep costsO(n)extra space. - In Rust, call associated functions with
Self::, and watch fori32overflow in growing products.
