Algorithm Patterns Guide
Algorithm Patterns Guide
Complete Reference
Two Pointers, Sweep Line, Greedy, Sliding Window, DFS/BFS,
Recursion
1
10. Quick decision checklist (use this in an interview)
11. Comparison with Two Pointers
Greedy Algorithms — Complete Guide
1. What is it?
2. Why is it used?
3. Where is it used?
4. How to identify a Greedy problem (pattern triggers)
5. Can you tell from constraints alone?
6. Intuition / Rationale
7. Time & Space Complexity (generic)
8. Types of Greedy (with Java templates)
9. Comparison of the patterns
10. Quick decision checklist (use this in an interview)
11. Comparison with Two Pointers and Sweep Line
Sliding Window — Complete Guide
1. What is it?
2. Why is it used?
3. Where is it used?
4. How to identify a Sliding Window problem (pattern triggers)
5. Can you tell from constraints alone?
6. Intuition / Rationale
7. Time & Space Complexity (generic)
8. Types of Sliding Window (with Java templates)
9. Comparison of the patterns
10. Quick decision checklist (use this in an interview)
11. Comparison with Two Pointers, Sweep Line, and Greedy
DFS & BFS — Complete Guide
1. What is it?
2. Why is it used?
3. Where is it used?
4. How to identify a DFS vs BFS problem (pattern triggers)
5. Can you tell from constraints alone?
6. Intuition / Rationale
7. Time & Space Complexity (generic)
8. Types of DFS & BFS (with Java templates)
2
9. Comparison of the patterns
10. Quick decision checklist (use this in an interview)
11. Comparison with Two Pointers, Sliding Window, Sweep Line, and Greedy
Recursion — Complete Guide
1. What is it?
2. Why is it used?
3. Where is it used?
4. How to identify a Recursion problem (pattern triggers)
5. Can you tell from constraints alone?
6. Intuition / Rationale
7. Time & Space Complexity (generic)
8. Types of Recursion (with Java templates)
9. Comparison of the patterns
10. Quick decision checklist (use this in an interview)
11. Comparison with DFS/BFS and the other patterns
3
Two Pointers — Complete Guide
1. What is it?
Two Pointers is a technique where you use two index variables (pointers) to traverse
a data structure (usually an array, string, or linked list) instead of nested loops, to
reduce time complexity from O(n²) to O(n) or O(n log n).
The pointers can move: - Toward each other (from both ends, converging) - In the
same direction (fast/slow, or a sliding window) - From different starting points
(merging two structures)
2. Why is it used?
3. Where is it used?
4
Signal in the problem statement What it suggests
“sorted array” / “sorted linked list” Converging or same-direction pointers
“pair/triplet that sums to X” Opposite-direction pointers
“in-place”, “O(1) space” Fast/slow same-direction pointers
“subarray/substring with property (sum, distinct
Sliding window (variable two pointers)
chars, at most K…)”
“remove duplicates”, “move zeroes” Slow/fast write-read pointers
“palindrome” Converging pointers from ends
“merge two sorted…” Two pointers, one per structure
“cycle in linked list”, “middle of linked list” Fast/slow (Floyd’s)
“container”, “trapping rain water”, “max area” Converging pointers, greedy shrink
Asks for O(n) or O(n log n) but brute force is
Strong hint to look for two pointers
O(n²) or O(n³)
Paraphrase test: If the problem can be rephrased as “find two indices i, j such that
some condition holds” or “find the longest/shortest contiguous range where condition
holds” — it’s very likely two pointers.
Yes, constraints are a strong secondary signal: - n up to 10^5 – 10^6 with expected
O(n) or O(n log n) → nested loops are out, two pointers/sliding window is a prime
candidate. - Array is stated or can be sorted without breaking the answer (order doesn’t
matter, e.g., “does a pair exist”) → sort + converging pointers. - If order matters and
you can’t sort (e.g., original indices needed) → use a hashmap instead, not two
pointers. - “At most/exactly/at least K distinct” with large n → sliding window (variant of
two pointers).
6. Intuition / Rationale
Nested loops recheck combinations blindly. Two pointers work because of a monotonic
invariant: moving one pointer only ever helps in one direction, so you never need to
re-examine a discarded state.
5
8. Types of Two Pointers (with templates)
Used for: sorted array pair sum, palindrome check, container with most water, trapping
rain water, reverse array in place.
6
public int longestSubarrayWithSumAtMostK(int[] arr, int k) {
int left = 0, sum = 0, maxLen = 0;
for (int right = 0; right < [Link]; right++) {
sum += arr[right];
while (sum > k) { // shrink window while invalid
sum -= arr[left];
left++;
}
maxLen = [Link](maxLen, right - left + 1);
}
return maxLen;
}
7
9. Comparison of the patterns
Typical trigger
Type Pointer movement Complexity
words
“sorted”, “pair sum”,
A. Opposite/ left++ or right– based
“palindrome”, O(n)
Converging on comparison
“container”
both move forward, “remove”, “in-place”,
B. Fast/Slow (in-place) O(n)
different speeds/roles “move zeroes”
“subarray”,
right always advances,
C. Sliding Window “substring”, “at most/ O(n)
left conditionally
exactly K”
fast moves 2x speed “cycle”, “linked list”,
D. Floyd’s Cycle O(n)
of slow “middle node”
independent pointers “merge”, “two sorted
E. Merge O(n+m)
on two structures arrays”
Key distinguishing question: Are there two separate collections (→ Merge), one
collection with a fixed-size shrink (→ Converging), one collection with a growable/
shrinkable range (→ Sliding Window), or a “rewrite in place” (→ Fast/Slow)?
If none of these fit and elements need to be looked up regardless of order/position, it’s
probably a hashmap problem instead, not two pointers. -e
8
Sweep Line — Complete Guide
1. What is it?
Sweep line is a technique where you imagine a vertical (or horizontal) line sweeping
across a set of events sorted by position (usually time, x-coordinate, or a numeric
range boundary), processing events in order and maintaining some running state (a
count, a running sum, an active set) as the line passes each event.
2. Why is it used?
3. Where is it used?
• Interval scheduling / meeting rooms (min rooms needed, can attend all meetings)
• Merge intervals, insert interval
• Skyline problem (building silhouette)
• Maximum overlapping intervals / max concurrent events
• Calendar booking (detect double-booking, k-booking)
• Car pooling / trip overlap capacity problems
• Computational geometry: closest pair of points, segment intersection, rectangle
area/union
• Range update problems solvable via difference arrays
9
4. How to identify a Sweep Line problem (pattern
triggers)
Yes: - Large n (up to 105–106) with intervals/points given → sorting-based O(n log n)
sweep is expected, since brute-force pairwise (O(n²)) is too slow. - If the problem only
asks for a count/aggregate at each moment (not exact geometric shapes) → simple
counter sweep suffices, no heap needed. - If it asks for the actual shape/skyline or
“which interval is active” → you need an ordered structure (heap or TreeMap) alongside
the sweep, not just a counter. - If updates apply to ranges of an array and you only
need the final array → difference array + prefix sum (a sweep variant) beats doing per-
index updates.
6. Intuition / Rationale
This is the same monotonicity idea as two pointers: once you sort events, you process
each boundary exactly once, and the running state (count, sum, or active set) is
updated incrementally rather than recomputed from scratch.
• Time: O(n log n) — dominated by sorting events (or the heap operations if used)
10
• Space: O(n) for the event list / heap / active-interval structure
Used for: meeting rooms II, max overlapping intervals, car pooling capacity check.
Used for: same problems as Type A, but generalizes better when you need to process by
exact timestamp order (ties handled explicitly — ends before starts at the same time, to
free a room before opening a new one).
11
public int minMeetingRoomsDelta(int[][] intervals) {
List<int[]> events = new ArrayList<>(); // {time, delta}
for (int[] iv : intervals) {
[Link](new int[]{iv[0], 1}); // start: +1 active
[Link](new int[]{iv[1], -1}); // end: -1 active
}
// Sort by time; if tie, process ends (-1) before starts (+1)
[Link]((a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
Used for: skyline problem, whether a new meeting fits in an existing room, k-booking.
Used for: “add value to all indices in [l, r]” applied many times, then read final array.
12
Type E — Coordinate Compression + Sweep (large/sparse
coordinate ranges)
Used for: skyline, rectangle union area, when coordinates span huge ranges but there
are few distinct values.
Typical trigger
Type Structure used Complexity
words
A. Counting (two two pointers on sorted “min rooms”, “max
O(n log n)
sorted arrays) starts/ends overlap”
B. Event list with sorted list of (time, same as A, but needs
O(n log n)
deltas ±1) tie-breaking control
“skyline”, “k-booking”,
C. Heap-based sweep min-heap of end times O(n log n)
“can attend”
“apply range update”,
plain array + prefix
D. Difference array fixed small coordinate O(n + q)
sum
range
E. Coordinate huge/sparse
compression + sorted map of deltas coordinate range, still O(n log n)
TreeMap need overlap count
Key distinguishing question: Do I just need a running count (→ Type A/B), do I need to
know which specific interval is “active” or ending soonest (→ Type C heap), am I
applying repeated range updates to array indices (→ Type D), or are coordinates too
large/sparse for a plain array (→ Type E)?
13
4. Are the coordinates huge or sparse (not a small bounded range)? → Type E
(coordinate compression)
5. Does the problem also require me to reconstruct the actual overlapping shape
(e.g. skyline outline), not just a count? → Type C, output changes on every heap-
top update, not just count changes
If ties at the same coordinate need special handling (e.g. does a meeting ending at 10
conflict with one starting at 10?), always decide this explicitly in your sort comparator
— it’s the most common sweep-line bug.
Sweep line is often thought of as “two pointers generalized to events with a start and
an end,” which is why Type A above looks almost identical to a two-pointer template. -e
14
Greedy Algorithms — Complete
Guide
1. What is it?
Greedy is a technique where at each step you make the locally optimal choice —
the choice that looks best right now — without reconsidering it later, and trust that the
sequence of locally optimal choices leads to a globally optimal solution.
Unlike DP, greedy never backtracks or explores alternatives — one pass, one decision
per step, done.
2. Why is it used?
• Much simpler and faster than DP or brute force when it’s provably correct (often
O(n log n) vs O(n²) or exponential)
• No extra memory for memoization tables
• Easy to reason about and implement once you’ve identified the right “greedy
criterion” (what to sort by / what to pick first)
3. Where is it used?
15
4. How to identify a Greedy problem (pattern triggers)
Paraphrase test: If you can rephrase the problem as “if I always pick the best option
available right now, will I never regret it later?” and you can convince yourself yes (via
an exchange argument or contradiction), it’s greedy.
Yes, partially: - If expected complexity is O(n log n) and DP would need O(n²) or worse,
that’s a hint towards greedy + sort. - Greedy problems rarely need extra memory
beyond O(1) or O(n) for sorting — if the intended solution needs a DP table, it’s usually
not pure greedy. - Caution: constraints alone can mislead you — many problems look
greedy but are DP in disguise (e.g. general coin change, 0/1 knapsack). Constraints only
tell you complexity expectations, not correctness of the greedy approach — you must
verify the greedy choice is provably safe (exchange argument or matroid structure), not
just assume it works because it’s fast.
6. Intuition / Rationale
Greedy works when the problem has two properties: 1. Greedy choice property — a
locally optimal choice can always be extended to a globally optimal solution (proved via
an exchange argument: assume an optimal solution doesn’t make the greedy choice,
show you can swap it in without making things worse). 2. Optimal substructure —
after making the greedy choice, the remaining subproblem is the same type of problem,
just smaller.
The exchange argument is the core proof technique: take any optimal solution, show
that replacing its first decision with the greedy decision doesn’t hurt (or only helps), and
repeat inductively. If you can’t construct this argument, greedy is likely wrong for the
problem — check DP instead.
16
Classic pitfall: 0/1 knapsack looks greedy (pick items with best value/weight ratio) but
isn’t — a locally best item can block a better combination later, because items can’t be
split. Fractional knapsack (where cutting off arbitrary weight is allowed) IS greedy,
because you never lose potential from a partial choice.
• Time: O(n log n) if sorting is required first, O(n) if no sort needed (e.g. single-pass
running-min/max problems)
• Space: O(1) extra beyond input/output, or O(n) if a heap/priority queue is used
int count = 0;
int lastEnd = intervals[0][1];
for (int i = 1; i < [Link]; i++) {
if (intervals[i][0] < lastEnd) {
count++; // overlap -> remove this interval (greedy: keep the one ending earlier)
} else {
lastEnd = intervals[i][1];
}
}
return count;
}
17
Type C — Reachability / Jump Game (extend the frontier greedily)
Used for: assign cookies, boats to save people (pair smallest with largest under a limit).
int totalCost = 0;
while ([Link]() > 1) {
int first = [Link]();
int second = [Link]();
int cost = first + second;
totalCost += cost; // greedy: always merge the two cheapest ropes first
[Link](cost);
}
return totalCost;
}
18
9. Comparison of the patterns
1. Can I sort the input by some criterion (end time, ratio, value) such that picking
greedily in that order is safe? → Type A/D
2. Am I just tracking a running best value while scanning once? → Type B
3. Is the question “how far can I get” or “is X reachable”? → Type C
4. Do I need to repeatedly combine the two best/cheapest available options? → Type
E (heap)
5. Can I construct an exchange argument — “if the optimal solution didn’t make
this choice, I could swap it in without making things worse”? If yes → greedy is
provably correct. If no, and especially if items are indivisible/discrete with
interacting constraints → suspect DP instead.
19
11. Comparison with Two Pointers and Sweep Line
In practice, greedy and two pointers/sweep line often coexist in the same solution —
sorting the input is a greedy setup step, and the pointer/sweep mechanics are how you
execute the greedy choices efficiently. If you’re identifying a problem as “greedy,” it’s
worth also asking whether the actual mechanics resemble a two-pointer or sweep-line
implementation. -e
20
Sliding Window — Complete Guide
1. What is it?
Sliding window is a technique where you maintain a contiguous range [left, right]
over an array or string, expanding it (moving right ) and shrinking it (moving left )
based on some condition, instead of re-scanning every possible subarray/substring from
scratch.
2. Why is it used?
3. Where is it used?
21
4. How to identify a Sliding Window problem (pattern
triggers)
Important caveat: sliding window with a simple expand/shrink works cleanly when the
tracked quantity is monotonic as the window grows — e.g., sum of non-negative
numbers only increases as you add elements. If negative numbers are allowed, growing
the window doesn’t monotonically increase the sum, so plain sliding window breaks —
look at prefix sums + hashmap instead.
Yes: - n up to 105–106 with an expected O(n) or O(n log n) solution, and the problem
mentions “subarray”/“substring” → sliding window is a prime candidate over brute
force. - If all values are non-negative (sums, counts, positive array elements) → sum/
count-based sliding window is safe. - If negative numbers are present and the problem
is about sum, sliding window alone won’t work — that’s usually prefix sum + hashmap
instead. - If the alphabet/character set is small and bounded (e.g., lowercase letters
only) → frequency-array window (size 26) instead of a hashmap, for O(1) per-character
updates.
22
6. Intuition / Rationale
The window is safe to slide because of a monotonic invariant, exactly like two
pointers: once the window’s property becomes invalid (e.g., sum too large, too many
distinct characters), shrinking from the left is the only useful move — you never need to
re-check a left boundary you’ve already advanced past, because any window starting
there was already accounted for as valid or invalid.
Each element is added to the window exactly once (when right passes it) and
removed exactly once (when left passes it), giving O(n) total work instead of O(n)
work repeated for O(n) different window start points.
Used for: max sum subarray of size K, moving average, max/min in window of size K
(with a deque).
Used for: longest subarray with sum ≤ K, longest substring with at most K distinct
characters.
23
public int longestSubarrayAtMostK(int[] nums, int k) {
int left = 0, sum = 0, maxLen = 0;
for (int right = 0; right < [Link]; right++) {
sum += nums[right];
while (sum > k) { // shrink while window is invalid
sum -= nums[left];
left++;
}
maxLen = [Link](maxLen, right - left + 1); // window is valid here
}
return maxLen;
}
Used for: minimum window substring, smallest subarray with sum ≥ target.
Used for: minimum window substring, find all anagrams, permutation in string.
24
public String minWindow(String s, String t) {
if ([Link]() || [Link]()) return "";
Map<Character, Integer> need = new HashMap<>();
for (char c : [Link]()) [Link](c, 1, Integer::sum);
while (have == needCount) { // window is valid, try to shrink from the left
if (right - left + 1 < bestLen) {
bestLen = right - left + 1;
bestStart = left;
}
char leftChar = [Link](left);
[Link](leftChar, [Link](leftChar) - 1);
if ([Link](leftChar) && [Link](leftChar) < [Link](leftChar)) {
have--; // shrinking broke the requirement for this character
}
left++;
}
}
return bestLen == Integer.MAX_VALUE ? "" : [Link](bestStart, bestStart + bestLen);
}
Used for: count subarrays with exactly K distinct elements, exactly K odd numbers, etc.
25
9. Comparison of the patterns
Typical trigger
Type Window behavior Complexity
words
slides one step at a “size K”, “moving
A. Fixed-size O(n)
time, constant width average”
B. Variable — longest expand right, shrink
“longest … at most K” O(n)
under a cap while invalid
C. Variable — shortest expand right, shrink “shortest/minimum …
O(n)
meeting a target while still valid at least”
tracks character/ “anagram”,
O(n) (O(26) or O(k) per
D. Frequency map element counts, not “permutation”,
step)
just a sum “contains all of t”
E. At-most-K two calls to Type B “exactly K distinct/
O(n)
subtraction variant odd/etc.”
Key distinguishing question: Is the window always the same width (→ A), am I
maximizing width under a constraint (→ B), minimizing width while meeting a threshold
(→ C), matching character frequencies (→ D), or counting subarrays with an EXACT
property (→ E, via the at-most trick)?
26
11. Comparison with Two Pointers, Sweep Line, and
Greedy
The cleanest mental model: two pointers is the general family (opposite-direction,
same-direction, merge, fast/slow). Sliding window is specifically the “same-direction,
contiguous range” member of that family, applied whenever the answer must be a
contiguous piece of the array/string. If contiguity doesn’t matter, you’re back to general
two pointers or a different technique entirely. -e
27
DFS & BFS — Complete Guide
1. What is it?
DFS (Depth-First Search) and BFS (Breadth-First Search) are the two fundamental
traversal techniques for graphs and trees.
• DFS goes as deep as possible down one path before backtracking — implemented
via recursion or an explicit stack.
• BFS explores level by level, visiting all neighbors at the current distance before
going further — implemented via a queue.
Both visit every reachable node exactly once (with a visited set), but the order and the
guarantees differ, which is what determines which one you need.
2. Why is it used?
3. Where is it used?
DFS: - Path existence / all paths between two nodes - Cycle detection (directed and
undirected graphs) - Topological sort - Connected components / islands (grid flood fill) -
Backtracking: permutations, combinations, subsets, N-Queens, Sudoku - Tree traversals
(preorder, inorder, postorder)
BFS: - Shortest path in unweighted graph / grid - Level-order tree traversal - Minimum
number of steps/moves (word ladder, sliding puzzle) - Multi-source spreading (rotting
oranges, walls and gates) - Bipartite graph checking (via 2-coloring while traversing
levels)
28
4. How to identify a DFS vs BFS problem (pattern
triggers)
Yes, partially: - Grid/graph size up to 104–106 nodes/cells with expected O(V+E) → either
DFS or BFS traversal is intended; the choice between them comes from what’s being
asked (shortest vs exhaustive), not from constraints. - If recursion depth could exceed
~10^4 (deep/skewed trees, long chains), a recursive DFS risks stack overflow in
Java — prefer iterative DFS with an explicit stack, or use BFS instead. - If the problem
says “minimum” or “shortest” AND edges are unweighted (all cost 1) → BFS, not DFS,
even though DFS could technically find a path — it won’t guarantee the shortest one
without extra bookkeeping. - If weights are involved despite looking like a shortest-path
problem → neither DFS nor BFS is sufficient; that’s a Dijkstra/Bellman-Ford signal.
6. Intuition / Rationale
Why BFS finds the shortest path: BFS processes nodes in strict order of distance
from the source — it fully exhausts all nodes at distance d before touching any node at
distance d+1 . The first time you reach the target, it’s guaranteed to be via the shortest
possible number of edges, because no longer path could have reached it first.
29
Why DFS is natural for exhaustive search: DFS’s call stack IS the current path
being explored. This makes it trivial to backtrack (undo the last choice and try another)
— which is exactly what problems like permutations, N-Queens, and path-finding-with-
constraints need. BFS’s queue doesn’t preserve “the current path” in the same
accessible way, making backtracking awkward.
Why both need a visited set: without it, cyclic graphs cause infinite loops — you’d
keep re-visiting the same nodes forever. Trees don’t strictly need one (no cycles), but
graphs always do.
• Time: O(V + E) for both — every vertex visited once, every edge examined once
• Space:
◦ DFS: O(V) for the visited set + O(H) for the recursion/explicit stack, where H is
the max depth (worst case O(V) for a skewed graph)
◦ BFS: O(V) for the visited set + O(W) for the queue, where W is the max width of
a level (worst case O(V))
Used for: same as Type A, but safe for very deep/large graphs.
30
public void dfsIterative(int start, Map<Integer, List<Integer>> graph) {
Set<Integer> visited = new HashSet<>();
Deque<Integer> stack = new ArrayDeque<>();
[Link](start);
while (![Link]()) {
int node = [Link]();
if ([Link](node)) continue;
[Link](node);
// process(node) here
Used for: shortest path in unweighted graph, level-order traversal, min steps.
while (![Link]()) {
int size = [Link](); // process one full level at a time
for (int i = 0; i < size; i++) {
int node = [Link]();
if (node == target) return steps;
31
public int numIslands(char[][] grid) {
int rows = [Link], cols = grid[0].length;
boolean[][] visited = new boolean[rows][cols];
int count = 0;
Used for: rotting oranges, walls and gates, spreading from multiple starting points
simultaneously.
32
public int multiSourceBFS(int[][] grid) {
int rows = [Link], cols = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int freshCount = 0;
int minutes = 0;
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
while (![Link]() && freshCount > 0) {
int size = [Link]();
for (int i = 0; i < size; i++) {
int[] cell = [Link]();
for (int[] d : dirs) {
int nr = cell[0] + d[0], nc = cell[1] + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
grid[nr][nc] = 2;
freshCount--;
[Link](new int[]{nr, nc});
}
}
}
minutes++;
}
return freshCount == 0 ? minutes : -1; // -1 if some fresh cells unreachable
}
33
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, new ArrayList<>(), new boolean[[Link]], result);
return result;
}
Typical trigger
Type Structure Complexity
words
“traverse”, “path
A. Recursive DFS call stack O(V+E)
exists”, “connected”
same as A, but very
B. Iterative DFS explicit Deque as stack O(V+E)
deep/large graphs
“shortest path”, “min
C. BFS Queue , level-by-level O(V+E)
steps”, “level order”
recursion over 2D “islands”, “flood fill”,
D. Grid DFS O(rows × cols)
array “connected region”
“simultaneously
queue seeded with
E. Multi-source BFS spread”, “min time to O(rows × cols)
multiple starts
reach all”
recursion + explicit “all permutations/
F. Backtracking DFS O(branching^depth)
undo step combinations/subsets”
1. Does the problem need shortest path / minimum steps in an unweighted graph? →
BFS (Type C)
34
2. Are there multiple simultaneous starting points spreading outward? → Multi-
source BFS (Type E)
3. Do I need to generate all valid combinations/arrangements, with the ability to undo
a choice? → Backtracking DFS (Type F)
4. Is it a 2D grid asking about connected regions/islands? → Grid DFS (Type D) (BFS
works too, DFS is usually simpler to write)
5. Is the graph very deep or could cause a stack overflow with recursion? → Iterative
DFS (Type B)
6. Otherwise — general traversal, cycle detection, topological sort → Recursive DFS
(Type A)
7. Are edges weighted and you still need shortest path? → Neither — use Dijkstra
(non-negative weights) or Bellman-Ford (negative weights allowed)
The core difference: earlier patterns (two pointers, sliding window, sweep line, greedy)
all operate on flat, ordered sequences where you never branch. DFS/BFS operate on
branching structures — there isn’t one path forward, there are potentially many, and
the choice of DFS vs BFS is about which order you explore those branches in. -e
35
Recursion — Complete Guide
1. What is it?
Recursion is when a function calls itself to solve smaller instances of the same
problem, until it reaches a base case simple enough to answer directly. The results of
the smaller calls are then combined to build the answer to the original problem.
Every recursive function needs exactly two parts: 1. Base case(s) — the condition(s)
where the function stops calling itself and returns directly. 2. Recursive case — where
the function calls itself on a smaller/simpler version of the input, and combines that
result into the current answer.
2. Why is it used?
• Naturally expresses problems that are self-similar — the solution to size n is built
from the solution to a smaller size
• Often dramatically simpler to read/write than the equivalent iterative version (tree
traversals, backtracking, divide and conquer)
• The call stack gives you “free” backtracking — undoing a choice is often just
returning from the call
• It’s the natural language for problems defined recursively in the first place (trees,
nested structures, mathematical recurrences)
3. Where is it used?
36
4. How to identify a Recursion problem (pattern triggers)
Paraphrase test: If you can finish the sentence “the answer to a problem of size n is
[some combination of] the answer(s) to problem(s) of smaller size” — that’s a recursive
definition, and the code should mirror it directly.
6. Intuition / Rationale
Recursion works because of mathematical induction: if you can prove the base case
is correct, and prove that the recursive case is correct assuming smaller sub-calls are
correct, then by induction the function is correct for all valid inputs. You don’t need to
trace every call by hand — you only need to trust the recursive leap of faith once the
base case and the inductive step are both verified.
The “leap of faith”: when writing return solve(n-1) + something; , you should not
mentally unroll what solve(n-1) does internally — you should assume it’s already
correct (by the inductive hypothesis) and focus only on how to correctly build the
answer for n from that assumed-correct result. This is the single biggest mental shift
that makes recursion easy to write instead of confusing.
37
Why the call stack matters: each recursive call gets its own stack frame with its own
local variables. This is what makes backtracking trivial — when a call returns, its local
state (e.g. [Link](x) before the call, [Link](x) after) naturally “undoes”
as control returns to the caller, without needing to manually manage a global undo
stack.
Used for: merge sort, quick sort, binary search, tree height/depth.
Used for: tree traversals, tree depth, validate BST, tree sum.
38
public int maxDepth(TreeNode root) {
if (root == null) return 0; // base case: empty tree has depth 0
private void backtrack(int[] nums, int start, List<Integer> current, List<List<Integer>> result) {
[Link](new ArrayList<>(current)); // every state along the way is a valid subset
Used for: Fibonacci, climbing stairs, any recursion with overlapping subproblems.
Used for: parsing recursive grammars, alternating state machines (less common in
interviews, but appears in parsers).
39
public boolean isEven(int n) {
if (n == 0) return true;
return isOdd(n - 1);
}
Key distinguishing question: Does the problem split into independent sub-ranges (→ B),
mirror a tree’s own shape (→ C), require generating all choices with undo (→ D), have
overlapping subproblems recomputed many times (→ E, add memoization), or is it a
straightforward “smaller version of the same problem” (→ A)?
1. Can I state the base case(s) first — the smallest input(s) I can answer without
recursing? Always start here.
2. Does my recursive case call the function on a strictly smaller input, guaranteeing
eventual termination? If not, infinite recursion.
3. Am I trusting the recursive call’s result (leap of faith) instead of trying to mentally
trace every nested call?
4. Do I need to undo a choice after exploring it (add to a list, then remove)? →
Backtracking (Type D)
5. Will the same sub-call happen with identical arguments more than once? → add a
memo map (Type E) before it becomes exponential
6. Is the recursion depth potentially very large (deep chains, large n)? → consider
converting to iteration to avoid StackOverflowError
40
7. Does the problem split cleanly into independent halves that don’t need to “know”
about each other until combined? → Divide and conquer (Type B)
The cleanest mental model: recursion is the umbrella technique. DFS is recursion
specifically applied to graph/tree adjacency. Backtracking is recursion specifically
applied to a decision tree where you must undo a choice to try the next one. Divide and
conquer is recursion where sub-calls are on independent, non-overlapping pieces of
input that get merged afterward. Memoized recursion (top-down DP) is recursion plus a
cache, used when sub-calls overlap. Once you see recursion as the shared foundation,
DFS/backtracking/divide-and-conquer/DP all become “recursion + one extra rule,” not
separate things to memorize from scratch. -e
41