Essential Recursive Algorithms and Backtracking Techniques
1) Factorial of n
Question
Compute n!.
Explain question
Return the product 1 * 2 * ... * n. The problem is naturally defined in terms of a smaller n.
Approach (in-depth)
If n is 0 (or 1) the answer is 1. Otherwise the factorial of n = n * factorial(n-1). The recursion reduces n by 1 on every call until the base case. No branching — just a single recursive call. This is linear recursion.
Recurrence relation
fact(n) = n * fact(n-1), with fact(0)=1.
Pseudocode
function fact(n):
if n == 0: return 1return n * fact(n-1)2) Print numbers from 1 to n (increasing)
Question
Print 1 2 ... N using recursion.
Explain question
You must produce an ordered sequence; recursion controls order by when you print relative to the recursive call.
Approach (in-depth)
If n == 0 stop. For increasing order: first recurse to print 1..(n-1), then print n. So recursion builds a stack of calls down to 0; printing happens during stack unwinding. For decreasing order, print before recursing.
Recurrence relation
printUp(n) : printUp(n-1); print n with base printUp(0) → no-op.
Pseudocode
function printUp(n):
if n == 0:
return printUp(n-1)
print(n)3) Print numbers from n to 1 (decreasing)
Question
Print n (n-1) ... 1 with recursion.
Explain question
Similar to above but reverse order — print first, recurse next.
Approach (in-depth)
If n == 0 stop. Print n first, then call printDown(n-1). This prints the current number before deeper calls.
Recurrence relation
printDown(n) : print n; printDown(n-1).
Pseudocode
function printDown(n):
if n == 0: returnprint(n)
printDown(n-1)4) Reverse a string using recursion
Question
Return the reverse of string s recursively.
Explain question
Reverse is defined by taking first char and placing it at the end of the reversed remainder.
Approach (in-depth)
Base case: empty or length 1 returns itself. Recursive step: reverse(s) = reverse(s[1:]) + s[0]. Note: naive substring-based implementation costs O(n²) due to string concatenation; for performance use a char[] and two-pointer swaps via recursion (swap left and right indices and recurse inward).
Recurrence relation
rev(s[0..N-1]) = rev(s[1..N-1]) + s[0]; base rev("") = "".
Pseudocode (simple)
function reverse(s):
if length(s) <= 1: return s
return reverse(s[1:]) + s[0]Pseudocode (efficient in-place style)
function reverseChars(arr, L, R):
if L >= R: return swap arr[L], arr[R]
reverseChars(arr, L+1, R-1)5) Check if an array is sorted (recursive)
Question
Return true if array A is strictly increasing.
Explain question
Check that every consecutive pair satisfies A[i] <= A[i+1]. Recursively reduce the array by one index.
Approach (in-depth)
Use an index i. Base case: i >= n-1 → true. At each step, compare A[i] and A[i+1]; if A[i] > A[i+1] return false; else recurse with i+1. This allows short-circuiting on first violation.
Recurrence relation
isSorted(i) = (A[i] <= A[i+1]) AND isSorted(i+1); base at i = n-1 → true.
Pseudocode
function isSorted(A, i):
if i >= len(A)-1: return trueif A[i] > A[i+1]: return falsereturn isSorted(A, i+1)6) Linear search in array (recursive)
Question
Return true/index if target exists in arr using recursion.
Explain question
Search linearly but via recursion. Each call checks one index and either returns or recurses.
Approach (in-depth)
Start at index i=0. If i == n return not found. If arr[i] == target, return found. Else recurse with i+1. Variants: find first index, all indices, or last index via unwinding.
Recurrence relation
search(i) = (arr[i]==target) OR search(i+1), base i == n → false.
Pseudocode
function search(arr, i, target):
if i == len(arr): return falseif arr[i] == target: return truereturn search(arr, i+1, target)7) First & last occurrence of an element (recursive)
Question
Find first index and last index of x in arr.
Explain question
First occurrence: search left-to-right; return upon first match. Last occurrence: use recursion to go to the end then choose deeper result when unwinding.
Approach (in-depth)
First:
if i==n return -1. If arr[i]==x return i else return first(i+1).
Last:
if i==n return -1. Recurse res = last(i+1); if res != -1 return res else if arr[i]==x return i else -1. This uses stack unwinding to prefer later indices.
Recurrence relation
First: first(i) = if arr[i]==x then i else first(i+1).
Last: last(i) = let r = last(i+1); if r!=-1 return r; else if arr[i]==x return i else -1.
Pseudocode
function first(arr, i, x):
if i == len(arr): return -1if arr[i] == x: return i
return first(arr, i+1, x)
function last(arr, i, x):
if i == len(arr): return -1 r = last(arr, i+1, x)
if r != -1: return r
if arr[i] == x: return i
return -1 8) Power function x^n (recursive, fast)
Question
Compute x^n.
Explain question
Use recursion to reduce exponent quickly via divide & conquer.
Approach (in-depth)
Naive recursion is x * x^(n-1) (linear depth). Fast exponentiation uses halving: compute half = power(x, n/2). If n even, result = half * half. If odd, result = half * half * x. Handle n==0 base. For negative exponents use reciprocals (if float/double).
Recurrence relation
power(n) = power(n/2)^2 (even); = x * power(n/2)^2 (odd); base power(0)=1.
Pseudocode
function power(x, n):
if n == 0: return 1 half = power(x, n // 2)if n % 2 == 0: return half * half
else: return x * half * half9) Tower of Hanoi
Question
Move n disks from peg A to peg C using B with legal moves.
Explain question
To move the largest disk you must first move the top n-1 off the source to the helper, move largest to destination, then move n-1 onto it.
Approach (in-depth)
Base: n==1 — move disk from A→C. Recursively: move(n-1, A, B, C), print move n A→C, then move(n-1, B, C, A). This yields minimal 2^n - 1 moves. Connects directly to recurrence T(n)=2T(n-1)+1.
Recurrence relation
T(n)=2*T(n-1)+1, with T(1)=1; so T(n)=2^n - 1.
Pseudocode
function hanoi(n, from, to, aux):
if n == 1:
print(move disk 1 from from to to)
return hanoi(n-1, from, aux, to)
print(move disk n from from to to)
hanoi(n-1, aux, to, from)10) Generate all permutations of a string/array (distinct)
Question
Return all permutations of nums or characters.
Explain question
We must generate every ordering without duplicates. The recursion explores positions one-by-one and fills them with unused elements.
Approach (in-depth)
Two common patterns:
Swap-based:
For indexifrom 0..N-1, swaparr[i]witharr[start], recursestart+1, then swap back. This does in-place generation, O(1) extra per level.Used array Pattern:
Maintainused[]andpath[]; loop all indices, pick unused, mark used, recurse, unmark. Whenpath.Length==nadd a copy.
Backtracking step removes the last choice (or swaps back) to try other numbers.
Recurrence relation
perm(k) generates (n-k) branches where k is current position: f(k) = sum_{for each unused i} f(k+1); base k==n → output.
Pseudocode (swap)
function permute(arr, start):
if start == n:
add copy of arr as a permutation
returnfor i = start..N-1:
swap(arr, start, i)
permute(arr, start+1)
swap(arr, start, i) // backtrack11) Generate all subsequences (subsets)
Question
Return all subsets (power set).
Explain question
Each element can be present or absent, independent of order — total 2^n subsets. Must preserve inclusion decisions.
Approach (in-depth)
Classic include/exclude recursion: define function backtrack(index, path). For each index you do:
- Exclude: call
backtrack(index+1, path). - Include:
path.Add(nums[index]), callbacktrack(index+1, path), thenpath.RemoveLast().
Add path (copy) to result at each call if you want all subsets incrementally, or only at base index==n.
Recurrence relation
f(i) = f(i+1) [exclude] + f(i+1) with nums[i] included.
Pseudocode
function subsets(nums):
backtrack(0, [])function backtrack(i, path)
:
if i == n: add copy(path); return backtrack(i+1, path) // excludepath.Add(nums[i])
backtrack(i+1, path) // includepath.RemoveLast()12) Print all paths in a maze/grid (Right & Down only)
Question
From (0,0) to (m-1,n-1) move only Right (R) or Down (D) and print all valid paths (with obstacles optionally).
Explain question
Because moves only progress right/down, no cycles; each path has (m-1) downs and (n-1) rights. Backtracking is simpler (no visited[] required).
Approach (in-depth)
Define dfs(i,j,path). If (i,j)==(m-1,n-1) print path. Else:
- If
j+1 < nand cell open:dfs(i, j+1, path + "R"). - If
i+1 < mand cell open:dfs(i+1, j, path + "D").
With obstacles, check cell value. The recursion branches until bounds.
Recurrence relation
paths(i,j) = paths(i+1,j) + paths(i,j+1) constrained by obstacles.
Pseudocode
function dfs(i, j, path):
if i == m-1 and j == n-1:
print pathreturnif j+1 < n and open(i, j+1): dfs(i, j+1, path + "R")
if i+1 < m and open(i+1, j): dfs(i+1, j, path + "D")13) N-Queens
Question
Place n queens on an n×n board so none attack one another; return all valid placements.
Explain question
We place queens row-by-row. Each placement restricts columns and diagonals for lower rows.
Approach (in-depth)
Backtrack by row:
backtrack(row):- If
row == n, construct board from queen positions and add to results. - For each
col0..N-1:- If
col,row-coldiagonal, androw+coldiagonal are free, place queen (mark column & diagonals), callbacktrack(row+1), then unmark.
- If
- If
Optimizations: maintain boolean arrays cols[n], diag1[2n] (row-col offset), diag2[2n] (row+col) for O(1) safety checks.
Recurrence relation
solutions(row) = sum_{col valid} solutions(row+1).
Pseudocode
function solveNQueens(n):
backtrack(0)function backtrack(row):
if row == n: add board; returnfor col in 0..N-1:
if safe(row, col):
place queen at (row,col)
backtrack(row+1)
remove queen at (row,col)14) Sudoku solver (9×9)
Question
Fill a partially filled 9×9 board so every row, column, and 3×3 block contains digits 1..9 exactly once.
Explain question
Each empty cell is a choice point: choose a number 1–9 that doesn’t conflict with row, column, block constraints. Backtrack when no valid number leads to a solution.
Approach (in-depth)
- Choose an empty cell (heuristic: choose one with fewest possibilities to speed up).
- For
numin 1..9:- If
numnot in thatrow,col, and3x3block → place it and recurse to next empty. - If recursion returns false, remove (
0) and try next number.
- If
- If no number works → return false (trigger backtracking)
.
Maintain rows[9][10], cols[9][10], boxes[9][10] boolean arrays to check validity in O(1). When placing, set flags; when removing, clear them.
Recurrence relation
solve(k) where k is number of empty cells filled so far; solve(k) = try each allowed digit for next empty cell and recurse.
Pseudocode
function solveSudoku(board):
if no empty cell: return true (r,c) = pick empty
for num in 1..9:
if safe to place num:
place num
if solveSudoku(board): return true remove num
return false 15) Rat in a Maze (4-directional with obstacles)
Question
Print all paths from (0,0) to (n-1,n-1) moving U/D/L/R through cells marked 1, not revisiting cells.
Explain question
This is a typical DFS with visited tracking to avoid cycles. Each step can move to 4 neighbors if inside and not visited and cell is open.
Approach (in-depth)
- Use a
visited[][]boolean set; mark starting cell visited. dfs(x,y,path): if(x,y)is target print path. Else iterate four directions (D,R,L,U):- Compute
nx,ny. If valid and open and not visited: mark visited,dfs(nx,ny,path+dir), thenvisited[nx][ny]=false(backtrack).
- Compute
- Ordering of directions changes printed order but not correctness.
Recurrence relation
paths(x,y) = sum_{neighbors valid} paths(nx,ny) with visited preventing repeat.
Pseudocode
function dfs(x,y):
if x==n-1 and y==n-1: print path; returnfor dir in [D,R,L,U]:
nx, ny = x+dx, y+dy
if valid and maze[nx][ny]==1 and not visited[nx][ny]:
visited[nx][ny]=true dfs(nx,ny)
visited[nx][ny]=false 16) Word Search in a grid (single word)
Question
Given board of chars and a word, does the word exist by sequentially adjacent (vertical/horizontal) characters, no reuse of cell.
Explain question
Start DFS from any cell matching the first char. Walk neighbors matching the next character, marking visited to avoid reusing cells. If you reach full word length, success.
Approach (in-depth)
For every cell (i,j):
- If
board[i][j] == word[0], calldfs(i,j, idx=0).dfs(x,y,idx): - If
idx == word.Lengthreturn true. - If out of bounds or char mismatch or visited → return false.
- Mark visited, explore 4 directions with
idx+1. If any returns true, propagate true; otherwise unmark visited and return false.
Recurrence relation
dfs(idx,x,y) = OR_{neighbors} dfs(idx+1,nx,ny) if board[x][y] == word[idx].
Pseudocode
function exist(board, word):
for each cell:
if dfs(cell, 0): return truereturn false
function dfs(x,y,idx):
if idx == len(word): return trueif out of bounds or board[x][y] != word[idx] or visited: return false visited[x][y]=truefor each neighbor:
if dfs(nx,ny,idx+1): return true visited[x][y]=falsereturn false 17) Combination Sum
Question
Given distinct candidates[] and target, return all unique combinations where numbers sum to target; numbers may be reused.
Explain question
Each candidate can be chosen multiple times. Order of numbers in a combination doesn’t matter (use index to avoid duplicates). This is a classic unbounded knapsack via backtracking.
Approach (in-depth)
Backtrack with parameters (index, remaining, path):
- Base: if
remaining == 0, add copy of path. - If
index == len(candidates)orremaining < 0return. - Two choices at each index:
Take current
path.Add(candidates[index]), recurse with sameindexandremaining - candidates[index](because reuse is allowed); thenpath.RemoveLast().Skip current
Recurse withindex+1and same remaining.
Sort candidates optionally (not required) and prune early when candidate > remaining.
Recurrence relation
f(index, rem) = f(index, rem - candidates[index]) [choose] + f(index+1, rem) [skip].
Pseudocode
function backtrack(index, rem):
if rem == 0: add copy(path); returnif index == n or rem < 0: return // choose
path.Add(candidates[index])
backtrack(index, rem - candidates[index])
path.RemoveLast()
// skip
backtrack(index+1, rem)18) Permutations of an array (distinct)
(duplicate of #10 but listed separately)
Question
Return all permutations of nums.
Explain question
Same as #10.
Approach (in-depth)
Either use swap-in-place or used[]+path; both already described. Swap-based is often compact and avoids used[].
Recurrence relation
Same as #10.
Pseudocode (swap)
permute(start):
if start == n: add copy(arr)
for i = start..N-1:
swap(arr[start], arr[i])
permute(start+1)
swap(arr[start], arr[i])19) Permutations II (with duplicates)
Question
Return unique permutations when nums may contain duplicates.
Explain question
Naive permutations will generate duplicates because identical elements at different indices produce same orderings; we must avoid duplicate branches.
Approach (in-depth)
Sort the array first. Use used[] boolean. In the loop:
- Skip
iifused[i]is true. Crucial rule:
ifi>0 && nums[i] == nums[i-1] && !Used[i-1]thencontinue. This ensures at the same recursion depth you only use the first of equal values to start a branch. After choosingnums[i], markused[i]=true, recurse, then unmark.
This rule prevents sibling branches from starting with equal numbers in different orders.
Recurrence relation
permUnique(path) = for each i not used and not skipped due to duplicate rule: choose i and permUnique(path + nums[i]).
Pseudocode
sort(nums)
backtrack(path):
if path.Size == n: add copy(path); returnfor i in 0..N-1:
if used[i]: continueif i>0 and nums[i]==nums[i-1] and not used[i-1]: continue used[i]=true; path.Add(nums[i])
backtrack(path)
used[i]=false; path.RemoveLast()20) Palindrome Partitioning
Question
Return all possible partitions of s such that every substring in partition is a palindrome.
Explain question
You must cut the string into contiguous pieces; each piece must be a palindrome. Explore all cut positions, pruning when substring is not a palindrome.
Approach (in-depth)
Backtrack by start index:
backtrack(start, path):- If
start == len(s): add copy of path to results. - For
endfromstarttolen(s)-1:- Check if
s[start..End]is palindrome (use helper or DP table). - If palindrome: add substring to path, recurse
backtrack(end+1, path), thenremoveLast()(backtrack).
- Check if
- If
Optimization: precompute isPal[start][end] with DP for O(1) palindrome checks (O(n²) precompute) to speed repeated checks.
Recurrence relation
part(start) = for end in start..N-1 if palindrome(start,end) then part(end+1); base part(n) = [[]].
Pseudocode
function partition(s):
precompute isPal[][] or use check backtrack(0, [])function backtrack(start, path):
if start == n: add copy(path); returnfor end in start..N-1:
if isPalindrome(s, start, end):
path.Add(s[start..End])
backtrack(end+1, path)
path.RemoveLast()Final notes & quick reminders
- For all backtracking problems: always push a copy (new list) into results (e.G.,
new ArrayList<>(path)), not the mutablepathreference. - For grid DFS tasks (Word Search, Rat in Maze): use
visited[][]or temporarily alter the board (e.G., setboard[x][y] = '#') and restore it after recursion. - For duplicate-handling (subsets II, permutations II): sort first and skip duplicates at the same recursion level.
- For performance: use memoization where the recursion recomputes the same state (Fibonacci), or precompute tables (palindrome DP), or use specialized structures (Trie for Word Search II).
- Convert pseudocode blocks to Java by replacing
removeLast()withpath.Remove(path.Size()-1), usingList<List<...>>and copying withnew ArrayList<>(path).
