Every recursive function needs:
1. Base case: when to stop (prevents infinite recursion)
2. Recursive case: break problem into smaller subproblem
3. Progress: each call must move toward the base case
example
// JavaScript — Sum of array using recursionfunction sumArray(arr, idx = 0) {
if (idx >= arr.length) return0; // base casereturn arr[idx] + sumArray(arr, idx + 1); // recursive case
}
// Power functionfunction power(base, exp) {
if (exp === 0) return1; // base caseif (exp % 2 === 0) {
const half = power(base, exp / 2);
return half * half; // O(log n)
}
return base * power(base, exp - 1);
}
# Pythondef sum_array(arr, idx=0):
if idx >= len(arr):
return0return arr[idx] + sum_array(arr, idx + 1)
def power(base, exp):
if exp == 0:
return1if exp % 2 == 0:
half = power(base, exp // 2)return half * half
return base * power(base, exp - 1)
output
sum_array([1,2,3,4]) → 10
power(2, 10) → 1024
Note Always identify the base case first. Stack overflow risk: Python default limit is 1000 frames (sys.setrecursionlimit can increase). For deep recursion, convert to iteration. The power function shows how recursion can achieve O(log n) — a common follow-up.
base caserecursive caserecursion fundamentalsstack overflowrecursive thinking
Permutations
syntax
Generate all orderings of elements.
Backtrack: at each position, try each unused element.
Swap or use a 'used'boolean array to track which elements are placed.
example
// JavaScriptfunction permute(nums) {
const result = [];
function backtrack(current, remaining) {
if (remaining.length === 0) {
result.push([...current]);
return;
}
for (let i = 0; i < remaining.length; i++) {
current.push(remaining[i]);
backtrack(current, [...remaining.slice(0, i), ...remaining.slice(i + 1)]);
current.pop(); // undo choice
}
}
backtrack([], nums);
return result;
}
# Pythondef permute(nums):
result = []
def backtrack(current, remaining):
ifnot remaining:
result.append(current[:])
returnfor i in range(len(remaining)):
current.append(remaining[i])
backtrack(current, remaining[:i] + remaining[i+1:])
current.pop()
backtrack([], nums)
return result
Note Time O(n! * n), Space O(n). The 'undo choice' step (current.pop()) is the hallmark of backtracking. For permutations with duplicates: sort first, and skip if nums[i] == nums[i-1] and nums[i-1] was not used in this branch. n! grows extremely fast — practical only for n ≤ ~10.
Choose k items from n without regard to order.
Backtrack: at each step, choose to include or exclude the current element.
Only pick elements with index >= start to avoid duplicates.
example
// JavaScriptfunction combine(n, k) {
const result = [];
function backtrack(start, current) {
if (current.length === k) {
result.push([...current]);
return;
}
for (let i = start; i <= n; i++) {
current.push(i);
backtrack(i + 1, current);
current.pop();
}
}
backtrack(1, []);
return result;
}
# Pythondef combine(n, k):
result = []
def backtrack(start, current):
if len(current) == k:
result.append(current[:])
returnfor i in range(start, n + 1):
current.append(i)
backtrack(i + 1, current)
current.pop()
backtrack(1, [])
return result
Note Time O(C(n,k) * k), Space O(k) for recursion depth. The key difference from permutations: use a 'start' parameter to only pick forward, preventing duplicate combinations. Optimization: prune when remaining elements < needed elements (n - i + 1 < k - len(current)).
combinationschoose k from nbacktracking combinationsC(n,k)
Subsets (Power Set)
syntax
Generate all 2^n subsets of a set.
Backtrack: for each element, choose to include it ornot.
Alternative: iterative bit manipulation.
example
// JavaScriptfunction subsets(nums) {
const result = [];
function backtrack(start, current) {
result.push([...current]); // every state is a valid subsetfor (let i = start; i < nums.length; i++) {
current.push(nums[i]);
backtrack(i + 1, current);
current.pop();
}
}
backtrack(0, []);
return result;
}
// Iterative approachfunction subsetsIterative(nums) {
const result = [[]];
for (const num of nums) {
const newSubsets = result.map(sub => [...sub, num]);
result.push(...newSubsets);
}
return result;
}
# Pythondef subsets(nums):
result = []
def backtrack(start, current):
result.append(current[:])
for i in range(start, len(nums)):
current.append(nums[i])
backtrack(i + 1, current)
current.pop()
backtrack(0, [])
return result
Note Time O(2^n * n), Space O(n) recursion depth. Unlike combinations, we add to result at every recursive call (not just when a condition is met). For subsets with duplicates: sort the input, then skip nums[i] == nums[i-1] when i > start. The iterative approach is elegant and avoids recursion.
Place n queens on an n×n board so no two attack each other.
Backtrack row by row. For each row, try placing a queen in each column.
Check conflicts: same column, same diagonal (row-col), same anti-diagonal (row+col).
example
// JavaScriptfunction solveNQueens(n) {
const results = [];
const cols = new Set(), diag1 = new Set(), diag2 = new Set();
function backtrack(row, board) {
if (row === n) {
results.push(board.map(r => '.'.repeat(r) + 'Q' + '.'.repeat(n - r - 1)));
return;
}
for (let col = 0; col < n; col++) {
if (cols.has(col) || diag1.has(row - col) || diag2.has(row + col)) continue;
cols.add(col); diag1.add(row - col); diag2.add(row + col);
board.push(col);
backtrack(row + 1, board);
board.pop();
cols.delete(col); diag1.delete(row - col); diag2.delete(row + col);
}
}
backtrack(0, []);
return results;
}
# Pythondef solve_n_queens(n):
results = []
cols, diag1, diag2 = set(), set(), set()
def backtrack(row, board):
if row == n:
results.append(['.' * c + 'Q' + '.' * (n - c - 1) for c in board])
returnfor col in range(n):
if col in cols or (row - col) in diag1 or (row + col) in diag2:
continue
cols.add(col); diag1.add(row - col); diag2.add(row + col)
board.append(col)
backtrack(row + 1, board)
board.pop()
cols.discard(col); diag1.discard(row - col); diag2.discard(row + col)
backtrack(0, [])
return results
output
solve_n_queens(4) → 2 solutions
['.Q..','...Q','Q...','..Q.'] and ['..Q.','Q...','...Q','.Q..']
Note Time O(n!), Space O(n). The diagonal trick: queens on the same diagonal have the same (row-col), same anti-diagonal have same (row+col). Using sets for O(1) conflict checking is more elegant than iterating over placed queens. This is the canonical backtracking problem.
Fill empty cells one at a time.
For each empty cell, try digits 1-9.
Check row, column, and3x3 box constraints.
Backtrack if no valid digit fits.
example
// JavaScriptfunction solveSudoku(board) {
function isValid(board, row, col, num) {
const ch = String(num);
for (let i = 0; i < 9; i++) {
if (board[row][i] === ch) returnfalse; // rowif (board[i][col] === ch) returnfalse; // columnconst boxR = 3 * Math.floor(row / 3) + Math.floor(i / 3);
const boxC = 3 * Math.floor(col / 3) + (i % 3);
if (board[boxR][boxC] === ch) returnfalse; // box
}
returntrue;
}
function solve(board) {
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
if (board[r][c] === '.') {
for (let num = 1; num <= 9; num++) {
if (isValid(board, r, c, num)) {
board[r][c] = String(num);
if (solve(board)) returntrue;
board[r][c] = '.';
}
}
returnfalse; // no valid digit → backtrack
}
}
}
returntrue; // all cells filled
}
solve(board);
}
# Pythondef solve_sudoku(board):
def is_valid(row, col, num):
ch = str(num)
for i in range(9):
if board[row][i] == ch: returnFalseif board[i][col] == ch: returnFalse
br = 3 * (row // 3) + i // 3
bc = 3 * (col // 3) + i % 3if board[br][bc] == ch: returnFalsereturnTruedef solve():
for r in range(9):
for c in range(9):
if board[r][c] == '.':
for num in range(1, 10):
if is_valid(r, c, num):
board[r][c] = str(num)
if solve(): returnTrue
board[r][c] = '.'returnFalsereturnTrue
solve()
output
Fills in all '.' cells with valid digits 1-9.
Note Time O(9^(empty cells)) worst case, but pruning makes it much faster in practice. Optimization: use sets for each row, column, and box to check validity in O(1) instead of O(9). Finding the cell with fewest candidates first (MRV heuristic) dramatically speeds up solving.
DFS from each cell. At each step, check if the current cell matches the next character.
Mark cell as visited (temporarily modify it), recurse in4 directions, then unmark.
example
// JavaScriptfunction exist(board, word) {
const rows = board.length, cols = board[0].length;
function dfs(r, c, idx) {
if (idx === word.length) returntrue;
if (r < 0 || r >= rows || c < 0 || c >= cols) returnfalse;
if (board[r][c] !== word[idx]) returnfalse;
const saved = board[r][c];
board[r][c] = '#'; // mark visitedconst found = dfs(r+1,c,idx+1) || dfs(r-1,c,idx+1)
|| dfs(r,c+1,idx+1) || dfs(r,c-1,idx+1);
board[r][c] = saved; // unmarkreturn found;
}
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++)
if (dfs(r, c, 0)) returntrue;
returnfalse;
}
# Pythondef exist(board, word):
rows, cols = len(board), len(board[0])
def dfs(r, c, idx):
if idx == len(word):
returnTrueif r < 0or r >= rows or c < 0or c >= cols:
returnFalseif board[r][c] != word[idx]:
returnFalse
saved = board[r][c]
board[r][c] = '#'
found = (dfs(r+1,c,idx+1) or dfs(r-1,c,idx+1)
or dfs(r,c+1,idx+1) or dfs(r,c-1,idx+1))
board[r][c] = saved
return found
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0):
returnTruereturnFalse
Note Time O(m * n * 4^L) where L = word length. The temporary cell modification is a clean alternative to a separate visited set. Always restore the cell after backtracking. Optimization: check if the board has all needed characters before starting DFS.
word searchgrid DFSbacktracking gridpath finding word