Lessons
1 · Foundations (Think Like a Programmer)
2 · Data Structures and Recursion
- 05Arrays and Array-Based Problem Solving
- 06Strings: Characters, Immutability, and Efficient Manipulation
- 07Linked Lists: Nodes, Pointers, and Pointer Reasoning
- 08Stacks and Queues
- 09Hashing: HashMap, HashSet, and Frequency Patterns
- 10Recursion and Backtracking
- 11Trees and Binary Search Trees
- 12Heaps and Priority Queues
3 · Core Algorithms and Reusable Patterns
4 · Advanced Interview Topics
Lesson 10 · Data Structures and Recursion
Recursion and Backtracking
Learn to think recursively: base cases, the call stack and recursion trees. Then use backtracking (choose, explore, undo) to generate subsets, permutations and combinations, avoid duplicates, prune the search and solve N-Queens.
For
Learners who are comfortable with loops, arrays and lists and want a solid foundation in recursion before moving to trees, sorting and dynamic programming.
Level
Before you start
Articles 3 to 9, especially Big O (article 4) and arrays and lists (articles 3 and 5). You do not need to have used recursion before.
On this page
Key takeaways
- Every recursive method needs a base case that stops the recursion, and a recursive case that moves closer to it. Trust the method on the smaller input.
- Each pending call lives on the call stack. Recursion that goes n levels deep uses O(n) stack space, and too much depth causes StackOverflowError.
- Draw the recursion tree. Its size tells you the running time: one call per level is linear, two calls per level is exponential.
- Backtracking means choose, explore, then undo the choice. Always copy a list before you save it, and always undo exactly what you did.
- Prune early (stop when a path cannot work) and skip equal values at the same level to avoid duplicate answers.
Imagine you are standing somewhere in a long queue and want to know how many people are ahead of you. You cannot see the front. So you ask the person in front: "How many people are ahead of you?" They ask the person in front of them, and so on. The first person in the queue says "zero, nobody". The next says "one", the next "two", and the answers travel back down the line until you hear yours.
Nobody had to see the whole queue. Each person only did two things: if I am first, answer 0; otherwise ask the person ahead and add one. That is recursion: solving a problem by solving a slightly smaller version of the same problem, until the problem becomes so small the answer is obvious.
Recursion is the key that unlocks trees, divide and conquer, dynamic programming, and a family of search problems called backtracking. It feels strange at first, which is normal. This article builds the intuition step by step.
What you will learn
#- The anatomy of a recursive method: base case and recursive case.
- How the call stack works, and what
StackOverflowErrormeans. - How to estimate running time by drawing a recursion tree.
- How to think recursively without tracing every call.
- The backtracking pattern: choose, explore, undo.
- How to generate subsets, permutations and combinations, avoid duplicates, prune, and solve N-Queens.
Anatomy of a recursive method
#Every recursive method has two parts:
- A base case: an input so small that the answer is immediate, with no further recursion.
- A recursive case: the method calls itself on a smaller input and uses that result to build the answer.
answer(problem): if problem is tiny: // base case return the obvious answer smaller = a slightly smaller problem // makes progress return combine(answer(smaller)) // recursive caseTwo things must be true or the recursion never ends:
- Every possible input eventually reaches a base case.
- Each recursive call moves toward a base case (smaller number, shorter list, fewer choices).
The classic first example is the factorial: n! = n × (n - 1) × ... × 1. The base case is 0! = 1 (and 1! = 1). The recursive case is n! = n × (n - 1)!.
The leap of faith
#Beginners often try to follow every call all the way down, and get lost. Instead use this mental trick: assume the method already works correctly for smaller inputs, and only check that this call uses that result correctly. For the factorial, assume factorial(n - 1) gives the right answer. Then n * factorial(n - 1) is clearly right. Plus the base case is right. Done. This is the same logic as mathematical induction, and it is how you design recursive solutions.
The call stack: what really happens
#When a method calls another method (including itself), the computer sets aside a block of memory for the new call, called a stack frame. It holds the parameters and local variables. Frames are stacked on top of each other. When a call returns, its frame is removed and the caller continues. This is exactly the stack data structure from Stacks and Queues.
Let us watch factorial(3). Time moves downward in this diagram.
call factorial(3) waits for factorial(2) call factorial(2) waits for factorial(1) call factorial(1) base case: returns 1 factorial(2) resumes: 2 * 1 = 2, returns 2factorial(3) resumes: 3 * 2 = 6, returns 6The following program shows the down phase (calls) and the up phase (returns) with printing, and some basic recursive functions:
// File: Main.javapublic class Main { static void trace(int n) { if (n == 0) { System.out.println("base"); return; } System.out.println("down " + n); trace(n - 1); System.out.println("up " + n); // runs after the deeper call returns } static long factorial(int n) { if (n <= 1) { return 1; // base case } return n * factorial(n - 1); // recursive case } static int sumDigits(int n) { if (n < 10) { return n; // one digit left } return n % 10 + sumDigits(n / 10); // last digit plus the digits of the rest } // Fast exponentiation: base^exp for exp >= 0. Halves the exponent each time. static long power(long base, int exp) { if (exp == 0) { return 1; } long half = power(base, exp / 2); if (exp % 2 == 0) { return half * half; } return half * half * base; } public static void main(String[] args) { trace(3); System.out.println(factorial(5) + " " + factorial(20) + " " + factorial(21)); System.out.println(sumDigits(9875) + " " + sumDigits(0)); System.out.println(power(2, 10) + " " + power(3, 0) + " " + power(5, 3) + " " + power(2, 62)); }}down 3down 2down 1baseup 1up 2up 3120 2432902008176640000 -424929004941921484829 01024 1 125 4611686018427387904Things to learn from the output:
- The
downlines come in order 3, 2, 1, thenbase, and then theuplines come in the opposite order. Code placed after the recursive call runs while the stack unwinds. Code before it runs while the stack grows. factorial(21)printed a negative number. The true value is larger than alongcan hold, so it wrapped around silently, just as withintoverflow in Java Essentials. For larger inputs you would needBigInteger.sumDigits(0)is0, andpower(2, 62)is exactly2^62. Going topower(2, 63)would overflow along.
Here is the dry run of power(2, 10). The exponent halves each time, so there are very few calls:
Call |
| Needs | Result when it returns |
|---|---|---|---|
| 10 |
|
|
| 5 |
|
|
| 2 |
|
|
| 1 |
|
|
| 0 | nothing, base case |
|
Five calls instead of ten multiplications. In general there are about log2(exp) + 1 calls, so the time is O(log exp). We use the same halving idea in binary search and in Bit Manipulation and Mathematical Patterns.
Stack depth and StackOverflowError
#Each pending call costs memory. Java gives a thread a limited stack, typically around one megabyte by default, enough for roughly some thousands to tens of thousands of simple frames. If recursion goes too deep, or never ends, Java throws StackOverflowError.
// File: Main.javapublic class Main { static int depth = 0; static void recurseForever() { // no base case! depth++; recurseForever(); } static long sumTo(int n) { if (n == 0) { return 0; } return n + sumTo(n - 1); // depth n } public static void main(String[] args) { System.out.println(sumTo(5000)); try { recurseForever(); } catch (StackOverflowError e) { System.out.println("StackOverflowError caught; reached more than 1000 levels: " + (depth > 1000)); } }}12502500StackOverflowError caught; reached more than 1000 levels: trueThe exact depth at which it fails varies with the machine and settings, so we only print that it went past 1,000. The practical lessons:
- A recursion that goes
nlevels deep uses O(n) space on the stack, even if it allocates nothing else. - A missing or unreachable base case causes a
StackOverflowError. - Java does not optimise away the stack for "tail" calls, so a loop is safer when the depth could reach tens of thousands. If an input of
n = 100,000would recurse that deep, convert the recursion into a loop, or use an explicit stack.
Drawing the recursion tree to find the running time
#Draw each call as a node, with its recursive calls as children. The total work is the work done in all the nodes.
- One call that shrinks by 1 (factorial,
sumTo): a straight line ofncalls. TimeO(n). - One call that halves the input (
power, binary search): a line of aboutlog ncalls. TimeO(log n). - Two calls that each shrink by 1 (naive Fibonacci): the tree doubles at every level. About
2^nnodes. TimeO(2^n). - Two calls that each handle half, plus linear work to combine (merge sort):
log nlevels,nwork on each level. TimeO(n log n).
fib(4)├── fib(3)│ ├── fib(2)│ │ ├── fib(1)│ │ └── fib(0)│ └── fib(1)└── fib(2) ├── fib(1) └── fib(0)You can see fib(2) appears twice. Repeated work like this is what dynamic programming removes, in Dynamic Programming Foundations. We counted the calls in the complexity article: fib(30) made 2,692,537 of them.
Recursion or iteration?
#Anything recursive can be written with a loop (and sometimes an explicit stack), and vice versa. Choose by clarity and safety:
Prefer recursion when | Prefer a loop when |
|---|---|
The problem is naturally self-similar (trees, nested structures, divide and conquer) | The recursion would go very deep (risk of |
You need to explore choices and undo them (backtracking) | A simple counter or accumulator does the job |
The recursive code is much shorter and clearer | You need the best possible speed and memory |
Backtracking: choose, explore, undo
#Some problems ask for all solutions, or the existence of one, among many possibilities: all subsets of a set, all orderings of items, all ways to place queens on a board. The brute-force idea is to build a solution one decision at a time and explore every option.
Backtracking is that idea, done carefully:
- Choose one option for the next decision.
- Explore by recursing to make the remaining decisions.
- Undo the choice (the "backtrack") so the next option starts from a clean state.
And it adds pruning: if a partial solution can already be seen to be impossible, abandon it immediately and skip everything below it.
backtrack(partial solution): if the partial solution is complete: record it return for each option available now: if the option is not allowed: // pruning continue apply the option // choose backtrack(updated partial solution) // explore undo the option // un-choosePicture it as a decision tree. Each node is a partial solution, each edge is a choice. Backtracking walks this tree depth first, and the "undo" step moves back up an edge.
Subsets
#Problem. Given distinct integers, return all subsets (the power set). For [1, 2, 3] there are 2^3 = 8: [], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3].
Approach. Keep a list current for the subset we are building. Every current we reach is itself a valid subset, so record it at the start of each call. Then, for each element from position start onwards, add it, recurse with start = i + 1 (so we never reuse or reorder elements), and remove it again.
The start parameter is what prevents [1, 2] and [2, 1] from both being generated: elements are only ever added in increasing index order.
// File: Main.javaimport java.util.ArrayList;import java.util.List;public class Main { static void subsets(int[] nums, int start, List<Integer> current, List<List<Integer>> result) { result.add(new ArrayList<>(current)); // copy! current keeps changing later for (int i = start; i < nums.length; i++) { current.add(nums[i]); // choose subsets(nums, i + 1, current, result); // explore current.remove(current.size() - 1); // undo: remove the LAST element by index } } // Variant for input with duplicates: sort first, then skip a value equal to the previous one at the same level. static void subsetsWithDuplicates(int[] sorted, int start, List<Integer> current, List<List<Integer>> result) { result.add(new ArrayList<>(current)); for (int i = start; i < sorted.length; i++) { if (i > start && sorted[i] == sorted[i - 1]) { continue; // same value already tried at this position } current.add(sorted[i]); subsetsWithDuplicates(sorted, i + 1, current, result); current.remove(current.size() - 1); } } public static void main(String[] args) { List<List<Integer>> all = new ArrayList<>(); subsets(new int[] {1, 2, 3}, 0, new ArrayList<>(), all); System.out.println(all.size() + " " + all); List<List<Integer>> unique = new ArrayList<>(); subsetsWithDuplicates(new int[] {1, 2, 2}, 0, new ArrayList<>(), unique); System.out.println(unique.size() + " " + unique); List<List<Integer>> none = new ArrayList<>(); subsets(new int[] {}, 0, new ArrayList<>(), none); System.out.println(none); }}8 [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]6 [[], [1], [1, 2], [1, 2, 2], [2], [2, 2]][[]]Three details deserve attention:
new ArrayList<>(current)makes a copy. If you wroteresult.add(current), every entry inresultwould be the same list object, which keeps changing and ends up empty or wrong. Always copy before you save.current.remove(current.size() - 1)removes by index. On aList<Integer>,remove(int)is an index andremove(Object)is a value, as we saw in the Java article. Here the argument is anint, so it is an index, which is what we want.- Duplicates. For
[1, 2, 2], without thecontinueline, the subset[1, 2]appears twice (once using the first2, once using the second), and[2]appears twice. After sorting, equal values sit together. The rulei > start && sorted[i] == sorted[i - 1]means: "if I am not the first option at this level, and I equal the one before me, skip me, because I would produce the same results".
Dry run of the duplicate-safe version on [1, 2, 2] (indexes 0, 1, 2):
Call ( | Loop does | Result added |
|---|---|---|
start 0, | records |
|
start 1, | records |
|
start 2, | records |
|
start 3, | records |
|
back in start 1 | index 2: | |
back in start 0 | picks index 1 ( | |
start 2, | records |
|
start 3, | records |
|
back in start 0 | index 2: |
Six subsets, no duplicates.
Complexity. There are 2^n subsets. Copying each takes up to n steps, so time is O(n × 2^n). The recursion depth is at most n, so extra space excluding the output is O(n). The output itself needs O(n × 2^n) space.
Permutations
#Problem. Return all orderings of distinct numbers. For [1, 2, 3] there are 3! = 6.
Here the choice at each step is "which not-yet-used element goes in the next position". A boolean[] used array records which are taken. Unlike subsets, we loop over all positions each time, skipping used ones.
Combinations
#Problem. Return all ways to choose k numbers from 1..n. For n = 4, k = 2: [1,2] [1,3] [1,4] [2,3] [2,4] [3,4].
This is like subsets, but we only record when the size reaches k, and we can prune: if there are not enough numbers left to fill the combination, stop looping. If we have current.size() numbers and need k - current.size() more, the next number can be at most n - (k - current.size()) + 1.
// File: Main.javaimport java.util.ArrayList;import java.util.List;public class Main { static void permutations(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> result) { if (current.size() == nums.length) { result.add(new ArrayList<>(current)); return; } for (int i = 0; i < nums.length; i++) { if (used[i]) { continue; // already placed } used[i] = true; // choose current.add(nums[i]); permutations(nums, used, current, result); // explore current.remove(current.size() - 1); // undo used[i] = false; } } static void combinations(int n, int k, int start, List<Integer> current, List<List<Integer>> result) { if (current.size() == k) { result.add(new ArrayList<>(current)); return; } int stillNeeded = k - current.size(); for (int i = start; i <= n - stillNeeded + 1; i++) { // pruning: leave room for the rest current.add(i); combinations(n, k, i + 1, current, result); current.remove(current.size() - 1); } } public static void main(String[] args) { List<List<Integer>> perms = new ArrayList<>(); permutations(new int[] {1, 2, 3}, new boolean[3], new ArrayList<>(), perms); System.out.println(perms.size() + " " + perms); List<List<Integer>> combos = new ArrayList<>(); combinations(4, 2, 1, new ArrayList<>(), combos); System.out.println(combos.size() + " " + combos); List<List<Integer>> few = new ArrayList<>(); combinations(3, 0, 1, new ArrayList<>(), few); System.out.println(few); }}6 [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]6 [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]][[]]The permutation order comes from the order of the loop. In the first few steps: pick 1, then from {2, 3} pick 2, then 3: [1, 2, 3] is recorded. Backtrack: unpick 3, unpick 2, pick 3 instead, then 2: [1, 3, 2]. And so on. The used array is what lets us "unpick" cleanly.
Complexity. There are n! permutations, each costing O(n) to copy: time O(n × n!). Depth n, so extra space O(n) beyond the output. For combinations, there are C(n, k) results, each of length k, so time O(k × C(n, k)).
Pruning matters here. Without the bound, the loop would also start branches that can never reach size k, wasting work. For subsets, permutations and combinations, the number of results is the real cost: if the output is exponential, no method can do better than exponential time.
Constraint-based search: N-Queens
#Problem. Place n queens on an n × n chessboard so that no two attack each other (no two share a row, a column or a diagonal). Count the arrangements.
Search. Place one queen per row, row by row. In each row, try every column. A square is allowed only if no earlier queen shares its column or either diagonal. If no column works for some row, we have a dead end, so return to the previous row and try its next column. This is backtracking with pruning at every step.
To check attacks in O(1), notice that:
- all squares on the same
/diagonal share the samerow + col; - all squares on the same
\diagonal share the samerow - col(we addn - 1to avoid negative indexes).
So three boolean arrays are enough: cols, diag1 (indexed by row + col) and diag2 (indexed by row - col + n - 1).
// File: Main.javapublic class Main { static int countSolutions(int n) { return place(0, n, new boolean[n], new boolean[2 * n - 1], new boolean[2 * n - 1]); } static int place(int row, int n, boolean[] cols, boolean[] diag1, boolean[] diag2) { if (row == n) { return 1; // all queens placed } int count = 0; for (int col = 0; col < n; col++) { int d1 = row + col; int d2 = row - col + n - 1; if (cols[col] || diag1[d1] || diag2[d2]) { continue; // attacked: prune this square } cols[col] = true; // choose diag1[d1] = true; diag2[d2] = true; count += place(row + 1, n, cols, diag1, diag2); // explore cols[col] = false; // undo diag1[d1] = false; diag2[d2] = false; } return count; } // Finds the first solution in the same search order, or null if none exists. static int[] firstSolution(int n) { int[] queenColumn = new int[n]; boolean found = search(0, n, queenColumn, new boolean[n], new boolean[2 * n - 1], new boolean[2 * n - 1]); return found ? queenColumn : null; } static boolean search(int row, int n, int[] queenColumn, boolean[] cols, boolean[] diag1, boolean[] diag2) { if (row == n) { return true; } for (int col = 0; col < n; col++) { int d1 = row + col; int d2 = row - col + n - 1; if (cols[col] || diag1[d1] || diag2[d2]) { continue; } cols[col] = true; diag1[d1] = true; diag2[d2] = true; queenColumn[row] = col; if (search(row + 1, n, queenColumn, cols, diag1, diag2)) { return true; // stop at the first solution } cols[col] = false; diag1[d1] = false; diag2[d2] = false; } return false; } public static void main(String[] args) { StringBuilder counts = new StringBuilder(); for (int n = 1; n <= 8; n++) { counts.append("n=").append(n).append(": ").append(countSolutions(n)).append(" "); } System.out.println(counts.toString().trim()); int[] solution = firstSolution(4); for (int row = 0; row < 4; row++) { StringBuilder line = new StringBuilder(); for (int col = 0; col < 4; col++) { line.append(solution[row] == col ? 'Q' : '.'); } System.out.println(line); } System.out.println(firstSolution(3) == null); }}n=1: 1 n=2: 0 n=3: 0 n=4: 2 n=5: 10 n=6: 4 n=7: 40 n=8: 92.Q.....QQ.....Q.trueThere are no solutions for n = 2 or n = 3, two for n = 4, and 92 for n = 8. Each queen's row is fixed (one per row), so we only choose columns. That alone cuts the search enormously: instead of choosing 8 squares from 64, we choose one column per row.
Complexity. The worst case is exponential, loosely O(n!), because each row has at most n choices and the number of choices shrinks as queens are placed. Pruning makes it far faster than that in practice. Time does not shrink to polynomial, but pruning keeps it practical: n = 12 (14,200 solutions) took about 55 milliseconds on the machine we tried it on. Space is O(n) for the arrays and the recursion.
How to recognise a backtracking problem
#Look for these signals:
- "Return all ..." (all subsets, all orderings, all valid strings).
- "Is there any way to ..." with many combinations to try.
- The input is small (usually
nup to about 10 to 20), because the answer count is exponential. - You build the answer step by step and each step has a few options.
- Some partial answers can be rejected early.
If the problem asks for a count or an optimum and there is repeated work in the tree (like fib(2) appearing twice), dynamic programming is usually the better tool. We study it in Dynamic Programming Foundations.
Complexity summary
#Problem | Number of results | Time | Extra space |
|---|---|---|---|
Factorial, | one value | O(n) | O(n) stack |
| one value | O(log n) | O(log n) stack |
Naive Fibonacci | one value | O(2^n) | O(n) stack |
Subsets |
| O(n × 2^n) | O(n) |
Permutations |
| O(n × n!) | O(n) |
Combinations ( |
| O(k × C(n, k)) | O(k) |
N-Queens (count) | varies | exponential, pruned | O(n) |
Edge cases and common mistakes
#- No base case, or a base case that is never reached. This ends in
StackOverflowError. - A recursive call that does not make the problem smaller. For example
f(n)callingf(n). - Forgetting to undo the choice. State from one branch leaks into the next and produces wrong results.
- Saving a reference instead of a copy (
result.add(current)). Alwaysnew ArrayList<>(current). - Mixing up
remove(int index)andremove(Object value)on aList<Integer>. - Duplicate input values causing duplicate answers. Sort and skip repeated values at the same level.
- Overflow in recursive arithmetic.
factorial(21)andpower(2, 63)exceed along. - Recursing too deeply. More than several thousand levels is risky. Use a loop or an explicit stack.
- Shared mutable state. If your recursion uses fields or arrays, make sure each call restores them exactly.
- Not thinking about the size of the output. If the answer has
2^nitems, annof 30 means over a billion results.
Exercises
#Exercise 1
Trace
#What does g(6) return? What does g(10) return? What does g(0) return, and is that sensible?
static String g(int n) { if (n == 0) { return ""; } return g(n / 2) + (n % 2);}Exercise 2: Binary strings without two adjacent ones
#Generate all binary strings of length n that do not contain two consecutive 1s. For n = 3: 000, 001, 010, 100, 101.
Exercise 3: Valid parentheses
#Generate all strings of n pairs of well-formed parentheses. For n = 3: ((())), (()()), (())(), ()(()), ()()().
Exercise 4: Debug
#This subsets method gives garbage output. Find the two bugs, say what it prints for [1, 2], and fix it.
static void subsetsBuggy(int[] nums, int start, List<Integer> current, List<List<Integer>> result) { result.add(current); for (int i = start; i < nums.length; i++) { current.add(nums[i]); subsetsBuggy(nums, i + 1, current, result); }}Exercise 5: Interview-style, combination sum
#Given distinct positive integers candidates and a positive target, return all unique combinations of candidates that sum to target. A candidate may be used any number of times. For [2, 3, 6, 7] and target 7 the answer is [2, 2, 3] and [7]. Explain how pruning helps.
Show solutionHide solution
g builds the binary representation of n.
g(6):g(3) + "0".g(3)isg(1) + "1".g(1)isg(0) + "1"which is"" + "1", so"1". Theng(3) = "11", andg(6) = "110".g(10):g(5) + "0",g(5) = g(2) + "1",g(2) = g(1) + "0",g(1) = "1". Sog(2) = "10",g(5) = "101",g(10) = "1010".g(0)returns the empty string"", because it is the base case. The binary form of zero should be"0", so the function is wrong for0. Fix by handling0separately in a wrapper:n == 0 ? "0" : g(n).
// File: Main.javapublic class Main { static String g(int n) { if (n == 0) { return ""; } return g(n / 2) + (n % 2); } static String toBinary(int n) { return n == 0 ? "0" : g(n); } public static void main(String[] args) { System.out.println("[" + g(6) + "] [" + g(10) + "] [" + g(0) + "] [" + g(1) + "]"); System.out.println(toBinary(0) + " " + toBinary(6) + " " + Integer.toBinaryString(10)); }}[110] [1010] [] [1]0 110 1010The recursion depth is the number of bits of n, about log2(n), so time and stack space are O(log n). (Building the string by repeated + costs a little more, O(log^2 n) in total, which is irrelevant at this size.)
Exercise 2
Show solutionHide solution
Build the string one character at a time. At each position you may append 0 freely. You may append 1 only if the previous character is not 1. That is a pruning rule that stops invalid strings at the earliest point.
The number of valid strings of length n follows the Fibonacci sequence (2, 3, 5, 8, 13, ...). Each valid string is generated once, so the time is O(n × count).
// File: Main.javaimport java.util.ArrayList;import java.util.List;public class Main { static void build(int n, StringBuilder current, List<String> result) { if (current.length() == n) { result.add(current.toString()); return; } current.append('0'); // 0 is always allowed build(n, current, result); current.deleteCharAt(current.length() - 1); boolean previousIsOne = current.length() > 0 && current.charAt(current.length() - 1) == '1'; if (!previousIsOne) { // 1 only after a 0 or at the start current.append('1'); build(n, current, result); current.deleteCharAt(current.length() - 1); } } static List<String> noAdjacentOnes(int n) { List<String> result = new ArrayList<>(); build(n, new StringBuilder(), result); return result; } public static void main(String[] args) { System.out.println(noAdjacentOnes(3)); System.out.println(noAdjacentOnes(1)); System.out.println(noAdjacentOnes(0)); System.out.println(noAdjacentOnes(4).size() + " " + noAdjacentOnes(5).size()); }}[000, 001, 010, 100, 101][0, 1][]8 13For n = 0, the only string is the empty string, so the answer is a list containing "". Choose and state this convention.
Exercise 3
Show solutionHide solution
The string has 2n characters. Track how many ( and ) have been used.
- You may add
(as long asopen < n. - You may add
)only ifclose < open, so there is an unmatched(to close. This is the pruning rule that keeps every prefix valid.
The number of results is the Catalan number: 1, 2, 5, 14, 42, ... for n = 1..5. Time is proportional to the output size times n, and recursion depth is 2n.
// File: Main.javaimport java.util.ArrayList;import java.util.List;public class Main { static void generate(int n, int open, int close, StringBuilder current, List<String> result) { if (current.length() == 2 * n) { result.add(current.toString()); return; } if (open < n) { current.append('('); generate(n, open + 1, close, current, result); current.deleteCharAt(current.length() - 1); } if (close < open) { current.append(')'); generate(n, open, close + 1, current, result); current.deleteCharAt(current.length() - 1); } } static List<String> generateParentheses(int n) { List<String> result = new ArrayList<>(); generate(n, 0, 0, new StringBuilder(), result); return result; } public static void main(String[] args) { System.out.println(generateParentheses(3)); System.out.println(generateParentheses(1)); System.out.println(generateParentheses(0)); for (int n = 1; n <= 5; n++) { System.out.print(generateParentheses(n).size() + " "); } System.out.println(); }}[((())), (()()), (())(), ()(()), ()()()][()][]1 2 5 14 42Exercise 4
Show solutionHide solution
Two bugs:
result.add(current)adds the list itself, not a copy. Every entry inresultpoints at the same list object, which keeps changing.- The choice is never undone. After
current.add(nums[i])and the recursive call, the element stays incurrent, so later iterations build on top of it.
For [1, 2], the buggy code records the same list four times, and by the end that list is [1, 2, 2] (the extra 2 because nothing was removed), so it prints that list four times. The fixed version copies and undoes.
// File: Main.javaimport java.util.ArrayList;import java.util.List;public class Main { static void subsetsBuggy(int[] nums, int start, List<Integer> current, List<List<Integer>> result) { result.add(current); for (int i = start; i < nums.length; i++) { current.add(nums[i]); subsetsBuggy(nums, i + 1, current, result); } } static void subsetsFixed(int[] nums, int start, List<Integer> current, List<List<Integer>> result) { result.add(new ArrayList<>(current)); for (int i = start; i < nums.length; i++) { current.add(nums[i]); subsetsFixed(nums, i + 1, current, result); current.remove(current.size() - 1); } } public static void main(String[] args) { List<List<Integer>> buggy = new ArrayList<>(); subsetsBuggy(new int[] {1, 2}, 0, new ArrayList<>(), buggy); System.out.println(buggy); List<List<Integer>> fixed = new ArrayList<>(); subsetsFixed(new int[] {1, 2}, 0, new ArrayList<>(), fixed); System.out.println(fixed); }}[[1, 2, 2], [1, 2, 2], [1, 2, 2], [1, 2, 2]][[], [1], [1, 2], [2]]Exercise 5
Show solutionHide solution
Think of it as a decision at each step: which candidate to add next, from start onwards. Because a candidate can be reused, the recursive call passes i (not i + 1) as the new start. Starting from start rather than from 0 stops us generating [2, 3] and [3, 2] as different answers.
Pruning. Sort the candidates. As soon as a candidate is larger than the remaining amount, every later candidate is also too large, so break out of the loop instead of trying them. Also stop recursing when the remaining amount hits 0 (record) or goes negative (we never allow it, thanks to the break).
The number of combinations can be exponential, so there is no polynomial bound. A common (loose) bound is O(N^(T/M)), where N is the number of candidates, T the target and M the smallest candidate: the deepest path adds the smallest candidate repeatedly. The recursion depth is at most T / M.
// File: Main.javaimport java.util.ArrayList;import java.util.Arrays;import java.util.List;public class Main { static void search(int[] sorted, int start, int remaining, List<Integer> current, List<List<Integer>> result) { if (remaining == 0) { result.add(new ArrayList<>(current)); return; } for (int i = start; i < sorted.length; i++) { if (sorted[i] > remaining) { break; // sorted, so all later ones are too big too } current.add(sorted[i]); // choose search(sorted, i, remaining - sorted[i], current, result); // i, not i + 1: reuse allowed current.remove(current.size() - 1); // undo } } static List<List<Integer>> combinationSum(int[] candidates, int target) { int[] sorted = candidates.clone(); Arrays.sort(sorted); List<List<Integer>> result = new ArrayList<>(); search(sorted, 0, target, new ArrayList<>(), result); return result; } public static void main(String[] args) { System.out.println(combinationSum(new int[] {2, 3, 6, 7}, 7)); System.out.println(combinationSum(new int[] {2, 3, 5}, 8)); System.out.println(combinationSum(new int[] {2}, 1)); System.out.println(combinationSum(new int[] {2, 3}, 6)); System.out.println(combinationSum(new int[] {}, 4)); }}[[2, 2, 3], [7]][[2, 2, 2, 2], [2, 3, 3], [3, 5]][][[2, 2, 2], [3, 3]][]For [2, 3, 6, 7] and target 7, the search tries 2, then 2, 2, then 2, 2, 2 (remaining 1: every candidate is larger, so break), then 2, 2, 3 (remaining 0: record). It backtracks and finally tries 7 alone. We clone the array before sorting so we do not modify the caller's input.
Where to go next
#- Next: Trees and Binary Search Trees. A tree is a recursive structure: every subtree is a smaller tree. Everything in this article applies directly.
- Later: Searching and Sorting uses recursion for merge sort and quick sort, and Dynamic Programming Foundations shows how to remove the repeated work from recursion.
- Practice habit: when you write a recursive method, first write one comment for the base case and one for the recursive step, in plain English, before you write any code.
Connected lessons
References
Linked from 5 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
How to Learn Data Structures and Algorithms Without Burning Out
Guide · Career
The OA Landscape: Platforms, Formats, Proctoring and Rules
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

