Thinking recursively
On This Lesson 11 topics
Lesson Overview
You have already met recursion in the methods module: a method that calls itself, and the call stack that remembers each waiting call. This lesson is about the thinking part. How do you look at a new problem and see the smaller copy of it hidden inside?
The picture to hold is a set of Russian dolls. To find out how many dolls there are, you open the biggest one. Inside is a smaller set of dolls. You do not need to understand the whole set at once: you only need to know what to do with one doll and what to do when there is no doll left. Everything else is the same question again, smaller.
This lesson gives you a short recipe for writing recursive methods, then applies it to digit sums, powers, and palindromes. It also shows how a recursive method can be much faster than a simple one, and what goes wrong when the recipe is skipped. The next lesson uses these ideas to generate every subset and every ordering.
This lesson builds on methods, the call stack, and recursion basics (BEG-014), and on Big O (BEG-039).
Learning Objectives
By the end of this lesson you will be able to:
- State the three parts of a recursive method: base case, smaller step, and combine.
- Write a recursive method from a plain-language description of the problem.
- Trace a recursive call by hand, both going down and coming back up.
- Explain what causes
StackOverflowErrorand how to prevent it. - Count the calls a recursive method makes and compare two recursive designs.
- Decide when a loop is a better choice than recursion.
Written Explanation
The recipe
To write a recursive method, answer three questions in order:
- Base case: what is the smallest version of the problem, whose answer I know immediately?
- Smaller step: how do I turn a bigger problem into a slightly smaller one of the same kind?
- Combine: if I trust that the smaller call returns the right answer, what do I do with it to get the answer for the bigger problem?
Take the sum of the digits of a non-negative number, such as 4821 giving 4 + 8 + 2 + 1 = 15.
- Base case: a number with a single digit (
n < 10) has a digit sum equal to itself. - Smaller step:
n / 10removes the last digit (integer division);n % 10is that last digit. - Combine: digit sum of
n= last digit + digit sum of the rest.
static int sumDigits(int n) {
if (n < 10) return n; // base case
return n % 10 + sumDigits(n / 10); // last digit + the rest
}The important mental move is step 3: trust the smaller call. You do not trace what happens inside sumDigits(482) while writing the method. You assume it returns 14 and ask "given that, what is the answer for 4821?" The answer: 1 + 14. When you debug, you can trace; when you design, you trust.
Tracing: down, then up
Tracing sumDigits(4821) shows two phases on the call stack:
sumDigits(4821) waits for sumDigits(482)
sumDigits(482) waits for sumDigits(48)
sumDigits(48) waits for sumDigits(4)
sumDigits(4) returns 4 <- base case
sumDigits(48) returns 8 + 4 = 12
sumDigits(482) returns 2 + 12 = 14
sumDigits(4821) returns 1 + 14 = 15Going down, each call waits. At the base case the first answer appears. Going up, each waiting call finishes its own combine step. The result 15 is what my run printed.
Factorial and the missing base case
static int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}factorial(4) returned 24. Without a base case the method never stops:
static int broken(int n) { return n + broken(n - 1); }Each call adds a new frame to the call stack and none returns. The JVM eventually throws StackOverflowError. In my run, try { broken(5); } catch (StackOverflowError e) { ... } caught it, and printed the name StackOverflowError. The Java API documentation describes this error as thrown when an application recurses too deeply. The exact depth at which it happens depends on the machine and settings, so do not rely on a particular number.
A base case that exists but is never reached has the same effect. For sumTo(n) with the base case if (n == 0), calling sumTo(-1) goes down forever, because n never equals 0. Writing n <= 0 is safer. The rule: every recursive call must move closer to the base case.
Order matters: before or after the call
Where you put the work changes the output. This code prints on the way down:
static void countdown(int n) {
if (n == 0) { System.out.println("Go!"); return; }
System.out.println(n);
countdown(n - 1);
}countdown(3) printed 3, 2, 1, Go!. If the print comes after the recursive call, it happens on the way up and the order reverses. A method with the print placed after countup(n - 1) printed 1, 2, 3 for countup(3). This idea (work before the call versus after) returns in the next lesson, where "undo" happens on the way back up.
Counting calls: simple versus halving
Consider power(b, e), "b to the power e":
static int power(int b, int e) {
if (e == 0) return 1;
return b * power(b, e - 1); // e calls deep
}For power(2, 10) the method makes 11 calls (e = 10 down to 0), so it takes about e steps: O(e).
A different smaller step: if e is even, b^e is (b^(e/2)) squared; if odd, one more factor of b.
static long fastPower(long b, int e) {
if (e == 0) return 1;
long half = fastPower(b, e / 2);
return e % 2 == 0 ? half * half : half * half * b;
}fastPower(2, 10) made only 5 calls (e = 10, 5, 2, 1, 0) and returned 1024, the same answer as power. Halving the problem each time takes about log₂(e) calls: O(log e). Notice we compute half once and reuse it; calling fastPower twice would lose the saving. A smaller step that cuts the problem by a fraction is far more powerful than one that cuts one item.
Recursion on text
Strings work well too: a palindrome reads the same in both directions.
- Base case: a string of length 0 or 1 is a palindrome.
- Smaller step: remove the first and last character (
substring(1, length - 1)). - Combine: it is a palindrome only if the first and last characters match and the middle is a palindrome.
static boolean isPalindrome(String s) {
if (s.length() < 2) return true;
if (s.charAt(0) != s.charAt(s.length() - 1)) return false;
return isPalindrome(s.substring(1, s.length() - 1));
}Results: "level" true, "abca" false, "" true. Note that substring(begin, end) includes begin and excludes end, according to the Java API documentation.
Recursion or a loop?
Everything above could also be written with a loop. Prefer recursion when the problem is naturally nested (folders in folders, trees) or when branching creates many paths (the next lesson). Prefer a loop when the work is a simple repetition over a long range, because each recursive call uses stack space and a very deep recursion can overflow.
Examples
Example 1: digit sum of a whole number (simple)
Calling sumDigits(4821) printed 15. The trace is the diagram above. A digit sum is a small piece of real work: some check-digit schemes for identification numbers add up digits (the specific schemes vary and are not covered here).
A boundary check: sumDigits(7) returns 7 at once, and sumDigits(10) returns 0 + sumDigits(1) = 1.
Example 2: reversing a String by recursion (practical)
Think in dolls: the reverse of "abc" is the reverse of "bc" followed by "a".
static String reverse(String s) {
if (s.length() <= 1) return s;
return reverse(s.substring(1)) + s.charAt(0);
}reverse("abc") printed cba and reverse("") returned an empty string. This is correct but each call creates a new string, so it is slower than the loop version for long text. It is good for learning the pattern, not for production.
Example 3: fast power and counting calls (realistic)
Cost and performance questions are part of the job. I added a counter to both versions:
| Call | Result | Calls made |
|---|---|---|
power(2, 10) | 1024 | 11 |
fastPower(2, 10) | 1024 | 5 |
fastPower(2, 11) | 2048 | 5 |
Doubling e adds only about one extra call to fastPower. The values grow too: with int, factorial(13) printed 1932053504, which is not the true value (6,227,020,800), because it does not fit in an int and overflows. Recursion fixes the structure, not the number range; use long or a bigger type when results can grow.
Backend Insight
Backend developers meet recursion through nested data: a JSON document that contains objects inside objects, a folder tree on a server, a menu with submenus, or comments with replies to replies. The shape "this thing contains smaller things of the same kind" is the signal. The trees you will study in the next modules are processed with exactly the recipe in this lesson.
Real services also need to guard against runaway recursion: a malicious or malformed input can nest very deeply and cause StackOverflowError. Production code often limits the depth or switches to a loop with an explicit stack. This course mentions that as a risk; the techniques are for later.
Common Mistakes
Mistake 1: No base case, or a base case never reached
The calls never stop and you get StackOverflowError. Check the smallest inputs, including 0 and negative numbers if they are possible.
Mistake 2: The recursive call does not shrink the problem
Calling sumDigits(n) instead of sumDigits(n / 10) does not move toward the base case. Ask: "is the argument strictly closer to the base case?"
Mistake 3: Forgetting to use the result
static int sumTo(int n) {
if (n <= 0) return 0;
sumTo(n - 1); // result ignored
return n;
}This compiles and returns just n. Make sure you combine the value the smaller call returns.
Mistake 4: Tracing too deep while designing
If you try to hold ten levels in your head, you will get lost. Write base case, smaller step, and combine, and test on small inputs.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Name the base case and the smaller step of factorial, and compute factorial(4).
Show solution A1
A1. Base case: n <= 1 returns 1. Smaller step: n - 1. factorial(4) = 4 * 3 * 2 * 1 = 24 (my run printed 24).
A2. Trace sumDigits(907). What does it return?
Show solution A2
A2. sumDigits(907) = 7 + sumDigits(90); sumDigits(90) = 0 + sumDigits(9); sumDigits(9) = 9. Back up: 9, then 0 + 9 = 9, then 7 + 9 = 16. It returns 16.
A3. What happens when a recursive method has no base case? Name the error.
Show solution A3
A3. The method keeps calling itself, each call adds a frame to the call stack, and eventually the JVM throws StackOverflowError.
Level B: Application
B1. Write sumTo(int n) that returns 1 + 2 + ... + n recursively (0 for n <= 0). What is sumTo(4)?
Show solution B1
B1.
static int sumTo(int n) {
if (n <= 0) return 0;
return n + sumTo(n - 1);
}sumTo(4) = 4 + 3 + 2 + 1 + 0 = 10 (my run printed 10).
B2. Write the recursive power(int b, int e) and say how many calls power(2, 10) makes.
Show solution B2
B2.
static int power(int b, int e) {
if (e == 0) return 1;
return b * power(b, e - 1);
}power(2, 10) makes 11 calls (e = 10 down to 0), which my counter confirmed.
B3. Trace the recursive isPalindrome on "abca" and say which call decides the answer.
Show solution B3
B3. isPalindrome("abca"): length 4, first a equals last a, so recurse on "bc". In that call b differs from c, so it returns false immediately. The second call decides, and the result false goes back up.
Level C: Challenge
C1. Write reverse(String s) recursively without a loop, and trace it on "abc".
Show solution C1
C1.
static String reverse(String s) {
if (s.length() <= 1) return s;
return reverse(s.substring(1)) + s.charAt(0);
}reverse("abc") = reverse("bc") + 'a' = (reverse("c") + 'b') + 'a' = "c" + 'b' + 'a' = "cba". My run printed cba.
C2. Count the calls of fastPower(2, 11), and explain why half is computed once and stored.
Show solution C2
C2. Calls for e = 11, 5, 2, 1, 0: 5 calls (my counter printed 5). If you wrote fastPower(b, e / 2) * fastPower(b, e / 2), each level would make two calls and the total would grow back to about e calls, losing the benefit. Storing half makes one call per level.
Mini Challenge
Write a recursive method int digitCount(int n) that returns how many digits a non-negative number has (treat 0 as 1 digit). Use the recipe and write down the base case first.
Check your work: digitCount(0) is 1, digitCount(9) is 1, digitCount(4821) is 4, digitCount(100) is 3. State how many calls it makes for a number with d digits.
Summary & Next Lesson
Key takeaways
- A recursive method needs a base case, a smaller step, and a combine step. Write them in that order.
- Trust the smaller call while designing; trace small inputs while debugging.
- A missing or unreachable base case causes
StackOverflowError. - Work placed before the recursive call happens on the way down; work placed after happens on the way up.
- Cutting the problem by a fraction (halving) can turn O(e) calls into O(log e).
- Use a loop for simple long repetition, recursion for nested or branching problems.
Next lesson
The next lesson, "Backtracking", combines recursion with a loop or two branches to generate all subsets and all permutations, undoing each choice on the way back up.
Self-check
- What are the three questions of the recursion recipe?
- Why can
sumTo(-1)run forever if the base case isn == 0? - What does "trust the smaller call" mean?
- How many calls does
fastPower(2, 10)make, and why fewer thanpower(2, 10)? - When is a loop a better choice than recursion?
Show answers
- Base case, smaller step, combine.
ngoes -1, -2, ... and never equals 0, so the base case is never reached. Usingn <= 0fixes it.- While writing the method, assume the recursive call returns the correct answer for the smaller problem, and only decide how to use it.
- 5 calls, because the exponent halves each time;
powerreduces it by one each time and needs 11 calls. - For simple repetition over a long range, where deep recursion would use a lot of stack space and may overflow.
References
- Oracle,
StackOverflowError(Java SE 21 API). It defines the error as thrown when a stack overflow occurs because an application recurses too deeply, which supports the base-case discussion. - Oracle,
String(Java SE 21 API). It definessubstring(beginIndex, endIndex)as inclusive of the begin index and exclusive of the end index. The recipe, the call counts, and the O(log e) estimate are this course's own analysis; the Java examples were compiled and run with JDK 21. Theintoverflow offactorial(13)was observed in my run and not checked against a source.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.