Recursion is when a function calls itself with a smaller or simpler input, trusting that the smaller problem is already solved β until it hits the simplest case (base case).
1. Base Case β The stopping condition. Without this, recursion runs forever (stack overflow).
2. Recursive Case β Calls itself with a smaller input, making progress toward base case.
// factorial(n) = n Γ factorial(n-1) // factorial(0) = 1 β base case int factorial(int n) { if (n == 0) return 1; // β BASE CASE (must exist!) return n * factorial(n - 1); // β RECURSIVE CASE } // factorial(4) expands as: // 4 Γ factorial(3) // 4 Γ 3 Γ factorial(2) // 4 Γ 3 Γ 2 Γ factorial(1) // 4 Γ 3 Γ 2 Γ 1 Γ factorial(0) β 1 // = 24 (answer unwinds back up)
- Must exist in every recursive function
- Must be reachable from every call chain
- Returns a concrete value (no more recursion)
- Usually handles n=0, n=1, empty array, or null
Segmentation Fault.
// β NO BASE CASE β crashes! int bad(int n) { return n * bad(n - 1); // runs forever }
// fib(0) = 0, fib(1) = 1 β TWO base cases // fib(n) = fib(n-1) + fib(n-2) int fib(int n) { if (n <= 0) return 0; // base case 1 if (n == 1) return 1; // base case 2 return fib(n-1) + fib(n-2); // recursive case } // Sum of first N numbers int sumN(int n) { if (n == 0) return 0; return n + sumN(n - 1); } // Power: a^b int power(int a, int b) { if (b == 0) return 1; // anything^0 = 1 if (b % 2 == 0) return power(a*a, b/2); // fast power O(log b) return a * power(a, b-1); }
Every recursive call spawns more calls β forming a tree. The depth of the tree = space complexity. The total nodes = time complexity.
Every function call creates a stack frame on the call stack β storing local variables, parameters, and the return address. Frames are pushed on call and popped on return.
- Parameters β values passed to the function
- Local variables β declared inside the function
- Return address β where to go after returning
- Return value β result passed back to caller
Default stack size is ~1-8 MB. Each frame uses memory. For n=100,000, factorial() creates 100,000 frames β stack overflow.
Recursive call is the last operation. No work happens after the call returns. Compiler can optimize to a loop (tail call optimization).
// Work done BEFORE recursive call void printN(int n) { if (n == 0) return; cout << n << " "; // work first printN(n - 1); // then call } // Prints: 5 4 3 2 1 // Tail recursive factorial (accumulator) int factTail(int n, int acc = 1) { if (n == 0) return acc; return factTail(n-1, n*acc); // call is last }
Recursive call happens first, work is done on the way back up (during unwinding). Cannot be tail-call optimized.
// Call first, THEN work void printReverse(int n) { if (n == 0) return; printReverse(n - 1); // call first cout << n << " "; // work after } // Prints: 1 2 3 4 5 // Same code as printN but reversed! // The call order flips the output.
Makes more than one recursive call per function. Creates a tree of calls. Classic: Fibonacci.
int fib(int n) { if (n <= 1) return n; return fib(n-1) + fib(n-2); // TWO calls β tree of calls // Time: O(2βΏ) without memo }
Function A calls function B, which calls A again. Rare in interviews but good to know.
void funcB(int n); // forward declare void funcA(int n) { if (n <= 0) return; cout << "A:" << n << " "; funcB(n - 1); // A calls B } void funcB(int n) { if (n <= 0) return; cout << "B:" << n << " "; funcA(n - 1); // B calls A }
| Aspect | Recursion | Iteration |
|---|---|---|
| Code clarity | Cleaner for tree/graph problems | Better for simple loops |
| Memory | O(n) stack frames | O(1) extra space |
| Speed | Slight overhead (function calls) | Faster (no call overhead) |
| Stack overflow risk | Yes β deep recursion crashes | None |
| Expressiveness | Natural for divide & conquer | Harder for tree traversal |
| Debugging | Harder β call stack is deep | Easier to trace |
| Use when | Trees, graphs, backtracking, D&C | Arrays, simple loops, O(1) space needed |
// Sum 1 to N β Recursive (O(n) space) int sumRec(int n) { if (n == 0) return 0; return n + sumRec(n - 1); } // Sum 1 to N β Iterative (O(1) space) int sumIter(int n) { int total = 0; for (int i = 1; i <= n; i++) total += i; return total; } // Sum 1 to N β Math formula (O(1) time+space) int sumMath(int n) { return n * (n + 1) / 2; }
Makes exactly one recursive call per invocation. Simplest form. Used for: traversal, counting, sum, reverse.
// Reverse an array using recursion void reverseArr(vector<int>& a, int l, int r) { if (l >= r) return; swap(a[l], a[r]); reverseArr(a, l + 1, r - 1); } // Check palindrome recursively bool isPalin(string& s, int l, int r) { if (l >= r) return true; if (s[l] != s[r]) return false; return isPalin(s, l+1, r-1); } // Count digits in a number int countDigits(int n) { if (n == 0) return 0; return 1 + countDigits(n / 10); }
Split problem into independent halves, solve each recursively, merge results. Examples: Merge Sort, Binary Search, Quick Sort.
void merge(vector<int>& a, int l, int m, int r) { vector<int> tmp; int i = l, j = m + 1; while (i <= m && j <= r) tmp.push_back(a[i] <= a[j] ? a[i++] : a[j++]); while (i <= m) tmp.push_back(a[i++]); while (j <= r) tmp.push_back(a[j++]); for (int k = l; k <= r; k++) a[k] = tmp[k-l]; } void mergeSort(vector<int>& a, int l, int r) { if (l >= r) return; // base case int m = l + (r - l) / 2; mergeSort(a, l, m); // left half mergeSort(a, m+1, r); // right half merge(a, l, m, r); // combine } // Time: O(n log n) | Space: O(n)
At each index, make a binary choice: include the element OR exclude it. Generates all subsets. Foundation of many backtracking problems.
void subsets(vector<int>& nums, int idx, vector<int>& cur, vector<vector<int>>& res) { if (idx == nums.size()) { res.push_back(cur); // base: record subset return; } // PICK nums[idx] cur.push_back(nums[idx]); subsets(nums, idx + 1, cur, res); // NOT PICK nums[idx] (backtrack) cur.pop_back(); subsets(nums, idx + 1, cur, res); } // For [1,2,3] generates 2Β³ = 8 subsets // [], [3], [2], [2,3], [1], [1,3], [1,2], [1,2,3]
Try a choice, recurse deeper, then undo the choice (backtrack) to try alternatives. Used when you need all valid solutions.
void permute(vector<int>& nums, int start, vector<vector<int>>& res) { if (start == nums.size()) { res.push_back(nums); return; } for (int i = start; i < (int)nums.size(); i++) { swap(nums[start], nums[i]); // CHOOSE permute(nums, start+1, res); // EXPLORE swap(nums[start], nums[i]); // UNDO (backtrack) } } // Combination Sum β pick same element multiple times void combSum(vector<int>& cands, int target, int idx, vector<int>& cur, vector<vector<int>>& res) { if (target == 0) { res.push_back(cur); return; } if (idx == cands.size() || target < 0) return; cur.push_back(cands[idx]); combSum(cands, target-cands[idx], idx, cur, res); // reuse cur.pop_back(); combSum(cands, target, idx+1, cur, res); // skip }
Recursively visit all children/neighbors. Foundation of tree traversals (inorder, preorder, postorder) and graph DFS.
struct Node { int val; Node*left, *right; }; void inorder(Node* root) { // Left Root Right if (!root) return; inorder(root->left); cout << root->val << " "; inorder(root->right); } void preorder(Node* root) { // Root Left Right if (!root) return; cout << root->val << " "; preorder(root->left); preorder(root->right); } int height(Node* root) { // DFS to find depth if (!root) return 0; return 1 + max(height(root->left), height(root->right)); }
Process array/string by advancing the index recursively. Replaces loops with recursive index movement.
// Sum array elements recursively int arrSum(vector<int>& a, int i) { if (i == a.size()) return 0; return a[i] + arrSum(a, i + 1); } // Reverse a string string revStr(string s, int i) { if (i == s.size()) return ""; return revStr(s, i+1) + s[i]; // head recursion! } // Find max in array int arrMax(vector<int>& a, int i) { if (i == (int)a.size() - 1) return a[i]; return max(a[i], arrMax(a, i + 1)); }
Cache results of recursive calls. If a subproblem is seen again, return cached value instead of recomputing. Converts exponential to polynomial.
unordered_map<int,int> memo; int fib(int n) { if (n <= 1) return n; if (memo.count(n)) return memo[n]; // cache hit! memo[n] = fib(n-1) + fib(n-2); // store result return memo[n]; } // Time: O(n) | Space: O(n) β huge improvement! // Cleaner with vector int fibMemo(int n, vector<int>& dp) { if (n <= 1) return n; if (dp[n] != -1) return dp[n]; return dp[n] = fibMemo(n-1,dp) + fibMemo(n-2,dp); } // Call: vector<int> dp(n+1,-1); fibMemo(n, dp);
| # | Problem | Pattern | Difficulty | Key Idea |
|---|---|---|---|---|
| 1 | Print 1 to N | Linear / Tail | Easy | Base: n=0, print then recurse or recurse then print |
| 2 | Factorial | Linear | Easy | n * factorial(n-1) |
| 3 | Fibonacci | Tree | Easy | Two base cases: fib(0)=0, fib(1)=1 |
| 4 | Power (a^b) | D&C | Easy | b even: power(aΒ²,b/2); odd: aΓpower(a,b-1) |
| 5 | Reverse Array | Two Pointer Rec | Easy | swap(a[l],a[r]), recurse(l+1,r-1) |
| 6 | Check Palindrome | Linear | Easy | s[l]==s[r] && isPalin(l+1,r-1) |
| 7 | Subsets / Power SetLeetCode 78 | Pick/Not Pick | Medium | 2 choices per element: include or skip |
| 8 | PermutationsLeetCode 46 | Backtracking | Medium | swapβrecurseβswap back |
| 9 | Combination SumLeetCode 39 | Backtracking | Medium | Can reuse elements; skip when target<0 |
| 10 | Letter CombinationsLeetCode 17 | Backtracking | Medium | Phone pad mapping, recurse each digit |
| 11 | Word SearchLeetCode 79 | DFS + Backtrack | Medium | Mark visited, explore 4 dirs, unmark |
| 12 | N-QueensLeetCode 51 | Backtracking | Hard | Place queen row by row, check validity |
| 13 | Sudoku SolverLeetCode 37 | Backtracking | Hard | Try 1-9 in each empty cell, backtrack on fail |
class Solution { bool isSafe(vector<string>& board, int r, int c, int n) { for (int i = r-1; i >= 0; i--) if (board[i][c] == 'Q') return false; // col for (int i=r-1, j=c-1; i>=0&&j>=0; i--,j--) if (board[i][j] == 'Q') return false; // diag left for (int i=r-1, j=c+1; i>=0&&j<n; i--,j++) if (board[i][j] == 'Q') return false; // diag right return true; } void solve(int row, int n, vector<string>& board, vector<vector<string>>& res) { if (row == n) { res.push_back(board); return; } for (int col = 0; col < n; col++) { if (isSafe(board, row, col, n)) { board[row][col] = 'Q'; // CHOOSE solve(row + 1, n, board, res); // EXPLORE board[row][col] = '.'; // UNDO } } } public: vector<vector<string>> solveNQueens(int n) { vector<string> board(n, string(n, '.')); vector<vector<string>> res; solve(0, n, board, res); return res; } };
| Algorithm | Time | Space (Stack) | Reason |
|---|---|---|---|
| Factorial(n) | O(n) | O(n) | n linear calls |
| Fibonacci(n) β no memo | O(2βΏ) | O(n) | 2 calls per node, depth n |
| Fibonacci(n) β with memo | O(n) | O(n) | Each subproblem solved once |
| Merge Sort | O(n log n) | O(n) | log n levels Γ n work per level |
| Binary Search | O(log n) | O(log n) | Halves each time |
| Subsets | O(2βΏ) | O(n) | 2 choices per element |
| Permutations | O(n!) | O(n) | n choices Γ (n-1) Γ ... Γ 1 |
| Power(a,b) fast | O(log b) | O(log b) | Halves exponent each call |
Time = (number of nodes in recursion tree) Γ (work per node)
Space = depth of recursion tree (max stack frames at one time)
Cache the result of each unique subproblem. On re-encountering it, return cached value. Keeps the recursive structure but eliminates repeated work.
Convert recursion to iteration. Fill a DP table from base cases upward. No function call overhead, no stack risk.
// 1. Plain recursion β O(2βΏ) int fib1(int n) { if(n<=1) return n; return fib1(n-1) + fib1(n-2); } // 2. Memoization β O(n) time, O(n) space int fib2(int n, vector<int>& dp) { if(n<=1) return n; if(dp[n]!=-1) return dp[n]; return dp[n] = fib2(n-1,dp) + fib2(n-2,dp); } // 3. Tabulation β O(n) time, O(n) space, no recursion int fib3(int n) { vector<int> dp(n+1); dp[0]=0; dp[1]=1; for(int i=2;i<=n;i++) dp[i]=dp[i-1]+dp[i-2]; return dp[n]; } // 4. Space optimized β O(n) time, O(1) space int fib4(int n) { if(n<=1) return n; int a=0, b=1; for(int i=2;i<=n;i++) { int c=a+b; a=b; b=c; } return b; }
- Missing base case. The #1 mistake. Always ask: "What is the smallest input where I know the answer directly?" That's your base case. Missing it = infinite recursion = crash.
- Not making progress toward base case. If
nnever decreases (or increases toward the base), recursion runs forever. Every call must get strictly closer to the base case. - Wrong return in backtracking. Forgetting to
pop_back()after recursion in backtracking problems. The undo step is critical β without it, you carry stale data into the next branch. - Not using the return value. Writing
recurse(n-1);instead ofreturn recurse(n-1);β the computed value is lost. If your function returns something, usereturn. - Recomputing subproblems. Plain Fibonacci is O(2βΏ) because it recomputes fib(3) many times. If you see overlapping subproblems in your recursion tree β add memoization immediately.
- Stack overflow on large inputs. If n > 10β΅ and you use plain recursion, expect stack overflow. Switch to iterative or increase stack size with
#pragma comment(linker, "/STACK:1000000000"). - Global state mutation without cleanup. Using global/static variables in recursion without resetting between branches leads to wrong answers in backtracking. Prefer passing state as parameters.