Topics › Basics › Learn Basic Recursion
Understand recursion by print something N times
Java source ↗ Edit this article ↗
Jump to code:
Problem
This is the first recursion exercise in the sheet, and it is deliberately tiny: print a fixed piece of text N times without a loop, using a function that calls itself. The real goal is to understand four things that every later recursive solution depends on:
- what a recursive call actually does to the program’s memory (the call stack),
- why every recursion needs a base case, and what happens without one,
- how a parameter that changes on each call makes the problem smaller,
- how the order of “do work” and “recurse” controls the order of the output.
To make the behaviour testable, the Java code builds the output into a String (one copy of the text per line) instead of printing directly; main asserts on that string and then prints it.
Constraints: 0 ≤ N ≤ 10^4 or so for the linear versions. Recursion depth is limited by the JVM stack, which is discussed below.
Example 1
text = "Hello", N = 3
output:
Hello
Hello
Hello
Example 2
text = "x", N = 0
output: (nothing)
What recursion is
A function is recursive when it calls itself. That alone would run forever, so a recursive function always has two parts:
- a base case: an input small enough that the answer is known without any further call;
- a recursive case: do a little work, then call the same function on a strictly smaller input, trusting it to handle the rest.
For “print N times”, the smaller input is “print N - 1 times” and the base case is “print 0 times”, which means do nothing.
Each call occupies a stack frame holding its parameters and local variables. When f(1) calls f(2), f(1) is paused, not finished; it resumes only when f(2) returns. The frames pile up until the base case is hit, then unwind in reverse order. This is why a recursion of depth N uses O(N) memory even if each frame is tiny, and why a missing base case ends in StackOverflowError.
Baseline: the loop
For comparison, this is what we are replacing.
static String iterative(String text, int n) {
StringBuilder out = new StringBuilder();
for (int i = 1; i <= n; i++) out.append(text).append('\n');
return out.toString();
}
Complexity. Time O(n), space O(1) beyond the output.
Approach 1: Recursion with a counter that goes up
Intuition. Mirror the loop exactly. The loop variable i becomes a parameter. The call countUp(i, n) means “print for the indices i, i+1, ..., n”. It prints once (for i) and delegates i+1..n to the next call. When i > n there is nothing left, which is the base case.
Algorithm.
- Start with
i = 1. - If
i > n, return (base case). - Append the text.
- Call yourself with
i + 1.
static String recursiveCountUp(String text, int n) {
StringBuilder out = new StringBuilder();
countUp(text, 1, n, out);
return out.toString();
}
static void countUp(String text, int i, int n, StringBuilder out) {
if (i > n) return; // base case: nothing left to print
out.append(text).append('\n'); // the work of this call
countUp(text, i + 1, n, out); // the same problem, one step smaller
}
Complexity. Time O(n), one call per line. Space O(n) for the call stack: all n + 1 frames are alive when the base case is reached, because every call is waiting for the one it made.
Note the pattern of a public wrapper plus a private helper with extra parameters. The caller should not have to know about i or the buffer; the wrapper supplies them.
Approach 2: Recursion with a counter that goes down
Intuition. Instead of counting from 1 to n, let the parameter itself be “how many lines are still to be printed”. countDown(n) prints one line and asks countDown(n - 1) for the rest; countDown(0) prints nothing. No separate index is needed, which is why most recursive definitions in mathematics are written this way (f(n) in terms of f(n - 1)).
Algorithm.
- If
n <= 0, return. - Append the text.
- Call yourself with
n - 1.
static String recursiveCountDown(String text, int n) {
StringBuilder out = new StringBuilder();
countDown(text, n, out);
return out.toString();
}
static void countDown(String text, int n, StringBuilder out) {
if (n <= 0) return;
out.append(text).append('\n');
countDown(text, n - 1, out);
}
Complexity. Time O(n), space O(n) stack.
The base case is written n <= 0, not n == 0. With n == 0 a negative argument would never reach the base case and would recurse until the stack overflows. Writing base cases as inequalities is a cheap habit that prevents a whole class of bugs.
Approach 3: Returning the result instead of sharing a buffer
Intuition. The previous two versions pass a StringBuilder down and mutate it, which is the efficient choice. A purely functional recursion returns its answer instead: “the output for n is one line followed by the output for n - 1”. It has no helper and no shared state, which makes the structure of the recursion as visible as it gets.
Algorithm.
- If
n <= 0, return the empty string. - Return
text + newline + (result for n - 1).
static String recursiveReturn(String text, int n) {
if (n <= 0) return "";
return text + "\n" + recursiveReturn(text, n - 1);
}
Complexity. Time O(n^2) characters copied in the worst case, because each level concatenates its line onto a string that grows with depth. Space O(n) stack plus the intermediate strings. Fine for understanding, wasteful for large n; prefer the buffer versions in real code.
Seeing the call stack
The helper below does no printing; it just logs when each call is entered and left. For n = 2 it produces
enter f(1)
enter f(2)
base case reached
leave f(2)
leave f(1)
static List<String> traceCalls(int n) {
List<String> log = new ArrayList<>();
trace(1, n, log);
return log;
}
static void trace(int i, int n, List<String> log) {
if (i > n) {
log.add("base case reached");
return;
}
log.add("enter f(" + i + ")");
trace(i + 1, n, log);
log.add("leave f(" + i + ")");
}
Read the log top to bottom: calls are entered in order 1, 2, and left in the reverse order 2, 1. Anything placed before the recursive call runs on the way down; anything placed after it runs on the way back up. Printing the text before the call (as all three approaches above do) gives the lines in forward order. Moving the print after the call would still produce n identical lines here, but as soon as the lines differ (printing i, for instance) the order flips. That single observation is the whole content of the next two problems in the sheet.
Edge cases and pitfalls
N = 0must print nothing. Every version handles it by hitting the base case immediately.- Negative
N. Usen <= 0(ori > n) as the base case so the recursion terminates;n == 0alone loops untilStackOverflowError. - Missing or unreachable base case. The most common recursion bug. Before writing the recursive call, write the line that returns without calling.
- Recursion depth. Each call costs a stack frame. The default JVM stack handles roughly ten to twenty thousand simple frames;
N = 10^6will overflow any of the linear versions here. The tests stay at2000. When depth is a concern, convert to a loop or restructure so the depth is logarithmic, as shown in Print name N times using recursion. - Shared mutable state. Passing the
StringBuilderdown is efficient but means every frame mutates the same object; the functional version avoids this at the price of extra copying. - Forgetting that the parameter must change.
countUp(text, i, n, out)instead ofi + 1recurses forever with the same argument.
Related problems
- Print name N times using recursion is the same exercise with the text as a parameter, plus a logarithmic-depth variant.
- Print 1 to N using Recursion and Print N to 1 using Recursion show how placing the print before or after the call flips the output order.
- Sum of First N Numbers and Factorial of a given number are the first recursions that return a value and combine it on the way back up.