Java Recursion
Java Recursion
A recursive method is a method that calls itself. Each call works on a smaller version of the problem until it reaches a case simple enough to solve directly — then the results unwind back through the call chain and combine into the final answer.
Recursion is not a trick. It is the natural way to express solutions to problems that are self-similar in structure: a folder contains files and other folders, a tree node has child nodes that are also trees, a string can be checked palindrome by comparing its ends and checking the middle. When the problem definition itself is recursive, recursive code often reads closer to that definition than any loop would.
How Recursion Works — The Two Requirements
Every correct recursive method has exactly two parts. Without both, it either never stops or never makes progress.
Base case — the condition where the method returns immediately without calling itself. It is the simplest version of the problem that can be solved directly. Without a base case, the method calls itself forever until the stack overflows.
Recursive case — the call the method makes to itself with a smaller or simpler version of the problem. Each recursive call must move closer to the base case. Without this progress, you have infinite recursion regardless of a base case.
Recursive structure of factorial(4):
factorial(4)
└─ 4 * factorial(3)
└─ 3 * factorial(2)
└─ 2 * factorial(1)
└─ base case: return 1
Unwinding (results flow back up):
factorial(1) = 1
factorial(2) = 2 * 1 = 2
factorial(3) = 3 * 2 = 6
factorial(4) = 4 * 6 = 24
First Example — Factorial
Factorial is the canonical starting point: n! = n * (n-1)! and 0! = 1. The definition is recursive — the right-hand side refers to factorial itself with a smaller input.
1// File: FactorialDemo.java
2
3public class FactorialDemo {
4
5 public static long factorial(int n) {
6
7 // Base case — simplest version, solved directly
8 if (n == 0 || n == 1) {
9 return 1;
10 }
11
12 // Recursive case — smaller subproblem
13 // n! = n * (n-1)!
14 return n * factorial(n - 1);
15 }
16
17 public static void main(String[] args) {
18
19 for (int i = 0; i <= 10; i++) {
20 System.out.printf("factorial(%2d) = %d%n", i, factorial(i));
21 }
22 }
23}Output:
factorial( 0) = 1
factorial( 1) = 1
factorial( 2) = 2
factorial( 3) = 6
factorial( 4) = 24
factorial( 5) = 120
factorial( 6) = 720
factorial( 7) = 5040
factorial( 8) = 40320
factorial( 9) = 362880
factorial(10) = 3628800
Each call to factorial(n) puts a new frame on the call stack — the value of n and the return address are saved. When the base case returns 1, the frames unwind in reverse order, each multiplying its n and returning the result upward. For factorial(4), exactly five stack frames are created and destroyed.
The Call Stack — What Happens in Memory
Understanding the call stack is what separates developers who use recursion confidently from those who fear it.
Call stack state when factorial(4) is executing: TOP OF STACK ┌────────────────────────────┐ │ factorial(1) → returns 1 │ ← currently executing (base case) ├────────────────────────────┤ │ factorial(2) → waiting │ ← waiting for factorial(1) ├────────────────────────────┤ │ factorial(3) → waiting │ ← waiting for factorial(2) ├────────────────────────────┤ │ factorial(4) → waiting │ ← waiting for factorial(3) ├────────────────────────────┤ │ main() → waiting │ ← waiting for factorial(4) └────────────────────────────┘ BOTTOM OF STACK Each frame stores: — the value of n for that call — the return address (where to go after this call returns) — local variables of that call
The JVM allocates a fixed stack space per thread — typically 512 KB to 1 MB by default. Deep recursion that creates tens of thousands of frames exhausts this space and throws StackOverflowError. This is why recursive solutions for very large inputs are replaced with iterative ones or with tail-call optimisation (which Java's JVM does not perform automatically).
Fibonacci — Naive Recursion and Its Problem
Fibonacci is a classic recursive problem — fib(n) = fib(n-1) + fib(n-2) — but the naive recursive implementation reveals the biggest performance risk of recursion: redundant recomputation.
1// File: FibonacciNaive.java
2
3public class FibonacciNaive {
4
5 static int callCount = 0;
6
7 // Naive recursion — recomputes the same subproblems many times
8 public static long fibNaive(int n) {
9 callCount++;
10
11 if (n <= 1) return n; // base cases: fib(0)=0, fib(1)=1
12 return fibNaive(n - 1) + fibNaive(n - 2);
13 }
14
15 public static void main(String[] args) {
16
17 System.out.println("Fibonacci sequence (naive):");
18 for (int i = 0; i <= 10; i++) {
19 callCount = 0;
20 long result = fibNaive(i);
21 System.out.printf("fib(%2d) = %3d | calls made: %d%n", i, result, callCount);
22 }
23
24 System.out.println("\nfib(30) call count:");
25 callCount = 0;
26 fibNaive(30);
27 System.out.println("fib(30) required " + callCount + " recursive calls");
28 }
29}Output:
Fibonacci sequence (naive):
fib( 0) = 0 | calls made: 1
fib( 1) = 1 | calls made: 1
fib( 2) = 1 | calls made: 3
fib( 3) = 2 | calls made: 5
fib( 4) = 3 | calls made: 9
fib( 5) = 5 | calls made: 15
fib( 6) = 8 | calls made: 25
fib( 7) = 13 | calls made: 41
fib( 8) = 21 | calls made: 67
fib( 9) = 34 | calls made: 109
fib(10) = 55 | calls made: 177
fib(30) required 2692537 recursive calls
fib(30) makes over 2.6 million calls to compute a number that fits in a single integer. fib(5) is computed 15 times inside fib(10). The time complexity is O(2ⁿ). For fib(50), this is over a trillion calls.
Memoization — Fixing Redundant Recomputation
Memoization stores the result of each subproblem the first time it is computed. Subsequent calls for the same input return the cached result immediately.
1// File: FibonacciMemo.java
2
3import java.util.HashMap;
4import java.util.Map;
5
6public class FibonacciMemo {
7
8 static int callCount = 0;
9
10 // Memoized recursion — O(n) time, O(n) space
11 public static long fibMemo(int n, Map<Integer, Long> memo) {
12 callCount++;
13
14 if (n <= 1) return n;
15
16 // Return cached result if already computed
17 if (memo.containsKey(n)) return memo.get(n);
18
19 // Compute, store, return
20 long result = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
21 memo.put(n, result);
22 return result;
23 }
24
25 public static void main(String[] args) {
26
27 System.out.println("Fibonacci with memoization:");
28 for (int i = 0; i <= 10; i++) {
29 callCount = 0;
30 long result = fibMemo(i, new HashMap<>());
31 System.out.printf("fib(%2d) = %3d | calls made: %d%n", i, result, callCount);
32 }
33
34 System.out.println("\nfib(30) call count with memoization:");
35 callCount = 0;
36 System.out.println("Result : " + fibMemo(30, new HashMap<>()));
37 System.out.println("Calls : " + callCount);
38
39 System.out.println("\nfib(50) with memoization:");
40 System.out.println("Result : " + fibMemo(50, new HashMap<>()));
41 }
42}Output:
Fibonacci with memoization:
fib( 0) = 0 | calls made: 1
fib( 1) = 1 | calls made: 1
fib( 2) = 1 | calls made: 3
fib( 3) = 2 | calls made: 5
fib( 4) = 3 | calls made: 7
fib( 5) = 5 | calls made: 9
fib( 6) = 8 | calls made: 11
fib( 7) = 13 | calls made: 13
fib( 8) = 21 | calls made: 15
fib( 9) = 34 | calls made: 17
fib(10) = 55 | calls made: 19
fib(30) call count with memoization:
Result : 832040
Calls : 59
fib(50) with memoization:
Result : 12586269025
fib(30) drops from 2.6 million calls to 59. The call count grows linearly with n — O(n). fib(50) is computed instantly. This is memoization: cache the result of each unique input, skip recomputation on repeated calls.
Recursion vs Iteration — Comparison Table
| Aspect | Recursion | Iteration |
|---|---|---|
| Code clarity | Matches problem structure — often clearer for hierarchical problems | Clearer for linear sequential operations |
| Memory usage | Stack frame per call — risk of StackOverflowError for deep problems | Constant — loop variables reused |
| Performance | Function call overhead per level | Generally faster — no call overhead |
| State management | JVM manages call stack automatically | Developer manages loop variables manually |
| Base case | Required — missing it causes StackOverflowError | Exit condition — missing it causes infinite loop |
| Debugging | Stack traces show call chain clearly | Loop state harder to inspect |
| Best suited for | Trees, graphs, divide-and-conquer, backtracking | Arrays, lists, counters, simple repetition |
| Tail-call optimisation | JVM does NOT optimise — every call uses a stack frame | Not applicable |
| Readability for tree/graph | High — mirrors the structure | Low — requires explicit stack simulation |
| Conversion | Any iteration can be made recursive | Any recursion can be converted to iteration with an explicit stack |
Binary Search — Recursion on a Sorted Array
Binary search divides the search space in half at each step — a naturally recursive structure. Each call searches a smaller portion of the array.
1// File: BinarySearchRecursive.java
2
3public class BinarySearchRecursive {
4
5 // Returns the index of target, or -1 if not found
6 public static int binarySearch(int[] arr, int target, int low, int high) {
7
8 // Base case — search space exhausted
9 if (low > high) return -1;
10
11 int mid = low + (high - low) / 2; // avoids integer overflow
12
13 if (arr[mid] == target) return mid; // found
14
15 if (target < arr[mid]) {
16 return binarySearch(arr, target, low, mid - 1); // search left half
17 } else {
18 return binarySearch(arr, target, mid + 1, high); // search right half
19 }
20 }
21
22 public static void main(String[] args) {
23
24 int[] sortedPrices = {299, 499, 799, 999, 1299, 1499, 1999, 2499, 3999, 4999};
25
26 int[] targets = {799, 1999, 100, 4999, 500};
27
28 System.out.println("Binary Search on sorted prices:");
29 System.out.println("Array: " + java.util.Arrays.toString(sortedPrices));
30 System.out.println();
31
32 for (int target : targets) {
33 int index = binarySearch(sortedPrices, target, 0, sortedPrices.length - 1);
34 if (index != -1) {
35 System.out.printf("Found Rs.%-5d at index %d%n", target, index);
36 } else {
37 System.out.printf("Rs.%-5d not found%n", target);
38 }
39 }
40 }
41}Output:
Binary Search on sorted prices:
Array: [299, 499, 799, 999, 1299, 1499, 1999, 2499, 3999, 4999]
Found Rs.799 at index 2
Found Rs.1999 at index 6
Rs.100 not found
Found Rs.4999 at index 9
Rs.500 not found
Each recursive call halves the search space — O(log n) time, O(log n) stack space. The base case low > high handles the not-found case. low + (high - low) / 2 avoids the integer overflow that (low + high) / 2 can cause when both values are large.
Real-World Example 1 — File System Directory Scanner
The Business Problem
A document management system at a company like Zoho or Freshworks needs to scan a directory tree — finding all files matching a pattern, computing folder sizes, or listing every file in a directory and all its subdirectories. A directory contains files and other directories. Other directories contain files and other directories. This self-similar structure makes recursion the natural fit.
1// File: FileSystemScanner.java
2
3import java.io.File;
4import java.util.ArrayList;
5import java.util.List;
6
7public class FileSystemScanner {
8
9 // Recursively collects all files under a directory matching an extension
10 public List<String> findFilesByExtension(File directory, String extension) {
11
12 List<String> found = new ArrayList<>();
13
14 if (!directory.exists() || !directory.isDirectory()) {
15 return found;
16 }
17
18 File[] entries = directory.listFiles();
19 if (entries == null) return found;
20
21 for (File entry : entries) {
22 if (entry.isFile() && entry.getName().endsWith("." + extension)) {
23 found.add(entry.getAbsolutePath()); // base case: it is a file
24 } else if (entry.isDirectory()) {
25 // Recursive case: go deeper into subdirectory
26 found.addAll(findFilesByExtension(entry, extension));
27 }
28 }
29
30 return found;
31 }
32
33 // Recursively calculates total size of all files in a directory tree
34 public long calculateDirectorySize(File directory) {
35
36 if (directory.isFile()) {
37 return directory.length(); // base case: return file size directly
38 }
39
40 long totalSize = 0;
41 File[] entries = directory.listFiles();
42
43 if (entries != null) {
44 for (File entry : entries) {
45 totalSize += calculateDirectorySize(entry); // recursive case
46 }
47 }
48
49 return totalSize;
50 }
51
52 // Recursively prints directory tree with indentation
53 public void printDirectoryTree(File directory, String indent) {
54
55 System.out.println(indent + directory.getName() + "/");
56
57 File[] entries = directory.listFiles();
58 if (entries == null) return;
59
60 for (File entry : entries) {
61 if (entry.isDirectory()) {
62 printDirectoryTree(entry, indent + " "); // deeper indent each level
63 } else {
64 System.out.println(indent + " " + entry.getName()
65 + " (" + entry.length() + " bytes)");
66 }
67 }
68 }
69
70 public static void main(String[] args) {
71
72 // Simulate on the current directory
73 File currentDir = new File(".");
74 FileSystemScanner scanner = new FileSystemScanner();
75
76 System.out.println("=== Java files in current directory tree ===");
77 List<String> javaFiles = scanner.findFilesByExtension(currentDir, "java");
78 if (javaFiles.isEmpty()) {
79 System.out.println("No .java files found.");
80 } else {
81 javaFiles.forEach(System.out::println);
82 }
83
84 System.out.println("\n=== Directory size ===");
85 long sizeBytes = scanner.calculateDirectorySize(currentDir);
86 System.out.printf("Total size: %.2f KB%n", sizeBytes / 1024.0);
87 }
88}Output:
=== Java files in current directory tree ===
./FileSystemScanner.java
./BinarySearchRecursive.java
./FactorialDemo.java
=== Directory size ===
Total size: 12.45 KB
The directory structure is inherently recursive — every directory is a potential parent of more directories. The recursive implementation mirrors this structure exactly: for each entry, if it is a file, handle it directly (base case); if it is a directory, recurse into it (recursive case). An iterative version would need an explicit stack to track which directories remain to be scanned.
Real-World Example 2 — Organisational Hierarchy Reporting
The Business Problem
An HR system at a services company like Infosys or Wipro needs to calculate total team size, compute total salary cost for a manager's entire reporting chain, and generate org chart output. An employee can manage other employees, who in turn manage their own teams. This reporting hierarchy is a tree — and trees are processed naturally with recursion.
1// File: Employee.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class Employee {
7
8 private final String employeeId;
9 private final String name;
10 private final String role;
11 private final double salary;
12 private final List<Employee> directReports;
13
14 public Employee(String employeeId, String name, String role, double salary) {
15 this.employeeId = employeeId;
16 this.name = name;
17 this.role = role;
18 this.salary = salary;
19 this.directReports = new ArrayList<>();
20 }
21
22 public void addDirectReport(Employee employee) {
23 directReports.add(employee);
24 }
25
26 public String getEmployeeId() { return employeeId; }
27 public String getName() { return name; }
28 public String getRole() { return role; }
29 public double getSalary() { return salary; }
30 public List<Employee> getDirectReports() { return directReports; }
31}1// File: OrgChartService.java
2
3public class OrgChartService {
4
5 // Recursively counts total headcount under a manager (including themselves)
6 public int getTotalHeadcount(Employee manager) {
7 int count = 1; // count the manager themselves
8 for (Employee report : manager.getDirectReports()) {
9 count += getTotalHeadcount(report); // recursive case
10 }
11 return count;
12 }
13
14 // Recursively calculates total salary cost of an entire reporting chain
15 public double getTotalSalaryCost(Employee manager) {
16 double total = manager.getSalary(); // include this employee's salary
17 for (Employee report : manager.getDirectReports()) {
18 total += getTotalSalaryCost(report); // recursive case
19 }
20 return total;
21 }
22
23 // Recursively finds an employee by ID anywhere in the hierarchy
24 public Employee findById(Employee root, String targetId) {
25 if (root.getEmployeeId().equals(targetId)) {
26 return root; // base case — found
27 }
28 for (Employee report : root.getDirectReports()) {
29 Employee found = findById(report, targetId);
30 if (found != null) return found; // found deeper in the tree
31 }
32 return null; // not in this branch
33 }
34
35 // Recursively prints the org chart with indented levels
36 public void printOrgChart(Employee employee, int level) {
37 String indent = " ".repeat(level);
38 System.out.printf("%s[%s] %s — %s | Rs.%,.0f/month%n",
39 indent,
40 employee.getEmployeeId(),
41 employee.getName(),
42 employee.getRole(),
43 employee.getSalary());
44
45 for (Employee report : employee.getDirectReports()) {
46 printOrgChart(report, level + 1); // go one level deeper
47 }
48 }
49}1// File: OrgChartDemo.java
2
3public class OrgChartDemo {
4
5 public static void main(String[] args) {
6
7 // Build the org hierarchy
8 Employee cto = new Employee("EMP-001", "Anjali Sharma", "CTO", 350000);
9 Employee vp1 = new Employee("EMP-002", "Rohan Mehta", "VP Engineering", 250000);
10 Employee vp2 = new Employee("EMP-003", "Priya Nair", "VP Product", 240000);
11 Employee mgr1 = new Employee("EMP-004", "Karan Singh", "Eng Manager", 180000);
12 Employee mgr2 = new Employee("EMP-005", "Sneha Rao", "Eng Manager", 175000);
13 Employee mgr3 = new Employee("EMP-006", "Amit Verma", "Product Manager", 165000);
14 Employee dev1 = new Employee("EMP-007", "Ravi Kumar", "Senior Dev", 140000);
15 Employee dev2 = new Employee("EMP-008", "Meera Iyer", "Senior Dev", 135000);
16 Employee dev3 = new Employee("EMP-009", "Suresh Yadav", "Dev", 110000);
17 Employee dev4 = new Employee("EMP-010", "Ananya Das", "Dev", 108000);
18 Employee pm1 = new Employee("EMP-011", "Deepak Joshi", "Associate PM", 120000);
19
20 // Build the tree
21 cto.addDirectReport(vp1);
22 cto.addDirectReport(vp2);
23 vp1.addDirectReport(mgr1);
24 vp1.addDirectReport(mgr2);
25 vp2.addDirectReport(mgr3);
26 mgr1.addDirectReport(dev1);
27 mgr1.addDirectReport(dev2);
28 mgr2.addDirectReport(dev3);
29 mgr2.addDirectReport(dev4);
30 mgr3.addDirectReport(pm1);
31
32 OrgChartService service = new OrgChartService();
33
34 System.out.println("=== Org Chart ===\n");
35 service.printOrgChart(cto, 0);
36
37 System.out.println("\n=== Team Statistics ===\n");
38 System.out.println("Total headcount under CTO : " + service.getTotalHeadcount(cto));
39 System.out.println("Total headcount under VP Eng : " + service.getTotalHeadcount(vp1));
40 System.out.printf("Total salary cost (CTO chain) : Rs.%,.0f/month%n",
41 service.getTotalSalaryCost(cto));
42 System.out.printf("Total salary cost (VP Eng) : Rs.%,.0f/month%n",
43 service.getTotalSalaryCost(vp1));
44
45 System.out.println("\n=== Find Employee ===\n");
46 Employee found = service.findById(cto, "EMP-008");
47 if (found != null) {
48 System.out.println("Found: " + found.getName() + " | " + found.getRole());
49 }
50 }
51}Output:
=== Org Chart ===
[EMP-001] Anjali Sharma — CTO | Rs.3,50,000/month
[EMP-002] Rohan Mehta — VP Engineering | Rs.2,50,000/month
[EMP-004] Karan Singh — Eng Manager | Rs.1,80,000/month
[EMP-007] Ravi Kumar — Senior Dev | Rs.1,40,000/month
[EMP-008] Meera Iyer — Senior Dev | Rs.1,35,000/month
[EMP-005] Sneha Rao — Eng Manager | Rs.1,75,000/month
[EMP-009] Suresh Yadav — Dev | Rs.1,10,000/month
[EMP-010] Ananya Das — Dev | Rs.1,08,000/month
[EMP-003] Priya Nair — VP Product | Rs.2,40,000/month
[EMP-006] Amit Verma — Product Manager | Rs.1,65,000/month
[EMP-011] Deepak Joshi — Associate PM | Rs.1,20,000/month
=== Team Statistics ===
Total headcount under CTO : 11
Total headcount under VP Eng : 7
Total salary cost (CTO chain) : Rs.19,73,000/month
Total salary cost (VP Eng) : Rs.9,98,000/month
=== Find Employee ===
Found: Meera Iyer | Senior Dev
The org chart is a tree. Every printOrgChart, getTotalHeadcount, getTotalSalaryCost, and findById call traverses a structure that is inherently hierarchical. The recursive solution mirrors the structure directly — one base case (leaf node with no reports), one recursive case (node with reports). An iterative version would need to manually manage a queue or stack to track which nodes remain to process.
Best Practices
Always define the base case before writing the recursive case. Think of it as the exit before the loop. Ask: what is the simplest input this method can receive where the answer is trivially known? That is your base case. Only then think about how to reduce a larger problem to that smaller form.
Ensure every recursive call moves toward the base case. The parameter passed to the recursive call must be strictly smaller or simpler than the current call's input. factorial(n - 1) is clearly closer to factorial(1) than factorial(n) is. If a recursive call could receive the same value as the current call under some condition, you have an infinite recursion risk.
Use memoization for problems with overlapping subproblems. If the same subproblem is computed more than once — like Fibonacci's fib(n-2) — cache the result. A HashMap<Integer, Long> passed as a parameter, or a class-level cache, prevents exponential blowup. This transforms O(2ⁿ) naive recursion into O(n) memoised recursion.
Convert to iteration when input size is unbounded. If the recursion depth could reach tens of thousands for valid inputs — processing a million-element list recursively, for example — use an iterative solution. Java's default stack size overflows well before a million frames. Deep recursion on user-controlled inputs is a denial-of-service vector.
Common Mistakes
Mistake 1 — Missing Base Case Causes StackOverflowError
1public static int countDown(int n) {
2 // No base case — this runs until the JVM stack fills up
3 System.out.println(n);
4 return countDown(n - 1); // always recurses, never stops
5}Exception in thread "main" java.lang.StackOverflowError
Every recursive call consumes a stack frame. Without a base case that returns, the method calls itself indefinitely until the stack space is exhausted. StackOverflowError is thrown by the JVM — not StackOverflowException. It is an Error, not an Exception, and should never be caught in normal code.
Mistake 2 — Recursive Case Does Not Reduce the Problem
1public static int badRecursion(int n) {
2 if (n == 0) return 0; // base case exists
3 return badRecursion(n); // recursive call — but n is unchanged!
4 // This never reaches n == 0 for any n > 0 — StackOverflowError
5}The base case exists but the recursive call does not reduce the input. badRecursion(5) calls badRecursion(5) which calls badRecursion(5) — infinite recursion. The recursive parameter must get closer to the base case with every call.
Mistake 3 — Naive Fibonacci in Production Code
1// Looks clean — runs in O(2^n) time
2public static long fib(int n) {
3 if (n <= 1) return n;
4 return fib(n - 1) + fib(n - 2); // recomputes every subproblem
5}
6
7// For fib(50): ~2^50 = 1,125,899,906,842,624 calls — never finishesNever use naive recursive Fibonacci in production. Use memoisation, an iterative approach, or the closed-form formula. fib(40) takes several seconds. fib(50) is practically infinite. This appears as a performance interview question specifically because candidates who write it must also be able to explain its flaw and fix it.
Mistake 4 — Catching StackOverflowError
1try {
2 result = process(input);
3} catch (StackOverflowError e) {
4 System.out.println("Stack overflow — input too deep.");
5 // Do NOT catch StackOverflowError in production
6}StackOverflowError signals that the program design is wrong — either the input is too large for a recursive approach, or there is a missing base case. Catching it masks the real problem. Fix the recursion depth by converting to iteration or adding proper base cases and input validation.
Interview Questions
Q1. What is recursion and what are the two required components of a recursive method?
Recursion is when a method calls itself to solve a smaller version of the same problem. Every correct recursive method requires two components: a base case — the simplest version of the problem that is solved directly without further recursion — and a recursive case — the call to itself with a strictly smaller or simpler input. Without the base case, the method runs forever and throws StackOverflowError. Without progress toward the base case, the same thing happens even if a base case is declared.
Q2. What is StackOverflowError and when does recursion cause it?
StackOverflowError is thrown by the JVM when the call stack runs out of space. Each recursive call adds a new frame to the stack — containing local variables and the return address. Java's default stack size is typically 512 KB to 1 MB per thread. Deep or infinite recursion — missing base case, non-reducing recursive call, or simply too large an input — exhausts this space. It is an Error, not an Exception, and should not be caught.
Q3. What is memoization and why is it important in recursion?
Memoization is caching the result of a recursive call so that if the same input is encountered again, the cached value is returned immediately without recomputation. It is critical for problems with overlapping subproblems — like Fibonacci — where naive recursion recomputes the same values exponentially many times. With memoization, Fibonacci's time complexity drops from O(2ⁿ) to O(n). Memoization is the top-down complement to bottom-up dynamic programming.
Q4. When should you use recursion instead of iteration?
Use recursion when the problem has inherently recursive structure — trees, graphs, divide-and-conquer algorithms, backtracking, and hierarchical data processing. Traversing an org chart, scanning a directory tree, implementing binary search on a sorted array, parsing JSON, and solving maze problems are all naturally recursive. Use iteration for linear sequential operations on arrays and lists, or when the recursion depth could be large enough to risk a stack overflow on valid inputs.
Q5. What is the difference between direct and indirect recursion?
Direct recursion is when a method calls itself directly — factorial() calls factorial(). Indirect recursion is when method A calls method B, which calls method A — a cycle of two or more methods. Both require the same two conditions: a base case and progress toward it. Indirect recursion is harder to trace and debug. A common example is mutual recursion: isEven(n) calls isOdd(n-1), which calls isEven(n-1), until reaching zero.
Q6. How would you convert a recursive solution to an iterative one?
Any recursive solution can be converted to an iterative one by using an explicit stack data structure — java.util.ArrayDeque — to manually manage what the call stack would have tracked automatically. Push the initial problem onto the stack. In a loop, pop the top, process the base case directly, and push subproblems back onto the stack for recursive cases. This eliminates the JVM stack frame risk for deep recursion while preserving the same logic.
FAQs
Can every recursive problem be solved iteratively?
Yes. Any computation expressible recursively can be expressed iteratively, and vice versa. The recursive solution uses the JVM's call stack implicitly. The iterative equivalent either uses loop variables for tail-recursive problems, or an explicit stack for general recursive ones. In practice, iterative solutions for deeply recursive problems avoid StackOverflowError and run faster due to lower overhead.
What is tail recursion and does Java optimise it?
Tail recursion is when the recursive call is the very last operation in a method — nothing is done with the return value except return it. In languages that support tail-call optimisation (TCO), the compiler replaces the tail call with a loop, eliminating the stack frame. Java's JVM does not perform TCO, so even tail-recursive methods in Java create a new stack frame per call. Tail recursion in Java provides no performance benefit over ordinary recursion.
What is the maximum recursion depth in Java?
It depends on the JVM stack size and the size of each stack frame (number and type of local variables). With default settings, around 5,000 to 10,000 frames is typical before StackOverflowError. The stack size can be increased using the JVM flag -Xss — for example, -Xss4m sets it to 4 MB. However, increasing stack size is a workaround for a design issue, not a solution.
Is recursion always slower than iteration in Java?
Not always, but usually in Java. Each recursive call involves pushing a stack frame, setting up local variables, and making a method call — all of which have overhead that a tight loop does not. For problems like tree traversal where the recursive structure closely matches the problem, the overhead is acceptable. For simple problems like factorial where iteration is trivial, iteration is faster. Memoized recursion for complex dynamic programming problems often outperforms iterative code that is harder to write correctly.
How does recursion work with multiple base cases?
A method can have multiple base cases. Binary search has two — low > high (not found) and arr[mid] == target (found). Fibonacci has two — fib(0) = 0 and fib(1) = 1. Having multiple base cases is normal — it means there are multiple simplest forms of the problem that can be answered directly. The key requirement is that every execution path either hits one of the base cases or makes a recursive call with a strictly smaller input.
Summary
Recursion is not a technique reserved for academic problems — it is the natural implementation strategy for any problem whose structure is self-similar. Directory trees, org charts, expression parsers, binary search trees, and divide-and-conquer algorithms all express their solutions more clearly with recursion than with loops.
The two rules that prevent every recursion bug: always define the base case first, and ensure every recursive call uses a strictly smaller or simpler input than the current call. Memoization turns exponentially slow naive recursion into linear-time solutions for overlapping subproblems. For very deep recursion on unbounded inputs, convert to iteration using an explicit stack.
For interviews, be ready to trace through a recursive call stack manually, explain StackOverflowError and when it occurs, implement and optimise a Fibonacci solution from naive to memoised, and describe when recursion is a better design choice than iteration.
What to Read Next
Learn how a method can accept any number of arguments.