DSA C++
DSA C++ Revision β†’ Recursion

Recursion β€” Think in Subproblems

A function that calls itself with a smaller input until it hits a base case. The backbone of trees, graphs, DP, and backtracking. Master this and unlock 70% of hard problems.

βˆžβ†’1Reduce & Conquer
7Patterns
20+Problems
O(2ⁿ)Worst Case
🧠
What is Recursion?
A function that solves a problem by solving smaller versions of itself
The Core Idea

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).

πŸ’‘ Real-life analogy: To find your place in a queue, ask the person in front β€” they ask the person in front of them β€” until the first person says "I am #1". Then answers flow back.
Two Mandatory Parts

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.

⚠️ Every recursion MUST move closer to the base case with each call. Otherwise β†’ infinite recursion.
C++ Factorial β€” Classic First Example
// 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)
factorial(4) β€” Step-by-Step Expansion
factorial(4)
β†’ calls factorial(3), waits...
factorial(3)
β†’ calls factorial(2), waits...
factorial(2)
β†’ calls factorial(1), waits...
factorial(1)
β†’ calls factorial(0), waits...
factorial(0) = 1
← BASE CASE hit! Returns 1
factorial(1) = 1Γ—1 = 1
← Unwinds upward
factorial(2) = 2Γ—1 = 2
←
factorial(3) = 3Γ—2 = 6
←
factorial(4) = 4Γ—6 = 24 βœ“
Final answer
πŸ›‘
Base Case & Recursive Case
The two pillars β€” get these wrong and nothing works
Base Case Rules
  • βœ…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
What Happens Without It
πŸ’₯ Stack Overflow! Each call occupies memory on the call stack. Without a base case, the stack fills up and crashes with Segmentation Fault.
C++Infinite Recursion
// ❌ NO BASE CASE β€” crashes!
int bad(int n) {
    return n * bad(n - 1); // runs forever
}
C++ Fibonacci β€” Multiple Base Cases
// 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);
}
🌳
Recursion Tree Visualization
See how calls expand β€” this is how you calculate complexity

Every recursive call spawns more calls β€” forming a tree. The depth of the tree = space complexity. The total nodes = time complexity.

fib(5) β€” Recursion Tree (notice repeated subproblems)
fib(5)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
fib(4)
fib(3) ⚠
β”Œβ”€β”€β”€β”€β”€β”€β”˜β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”˜β”€β”€β”€β”€β”
fib(3)
fib(2) ⚠
fib(2) ⚠
fib(1)=1
β”Œβ”€β”€β”˜β”€β”€β”
fib(2) ⚠
fib(1)=1
β”Œβ”˜β”
fib(1)=1
fib(0)=0
⚠ fib(3) computed 2Γ— ⚠ fib(2) computed 3Γ— Total calls: 2ⁿ β€” exponential! Fix: Memoization β†’ O(n)
🎯 Key insight: When you see overlapping subproblems in a recursion tree β†’ that's a signal to use memoization (DP). The recursion tree IS how you discover DP solutions.
πŸ“š
Stack Frame & Memory
What actually happens inside RAM when recursion runs

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.

Call Stack for factorial(4) β€” Push Phase β†’ Pop Phase
PUSH (building up)
factorial(4) n=4
factorial(3) n=3
factorial(2) n=2
factorial(1) n=1
factorial(0) β†’ 1
↑ Stack grows downward
β†’
POP (unwinding)
factorial(4) = 4Γ—6 = 24
factorial(3) = 3Γ—2 = 6
factorial(2) = 2Γ—1 = 2
factorial(1) = 1Γ—1 = 1
factorial(0) = 1 ← starts here
↑ Returns flow upward
Stack Frame Contains
  • πŸ“Œ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
Stack Overflow

Default stack size is ~1-8 MB. Each frame uses memory. For n=100,000, factorial() creates 100,000 frames β†’ stack overflow.

⚠️ In competitive programming: if n > 10⁡ and you use plain recursion β†’ likely TLE or MLE. Use iteration or increase stack size.
πŸ”€
Types of Recursion
Know the difference β€” it affects optimization and behavior
Tail Recursion Optimizable

Recursive call is the last operation. No work happens after the call returns. Compiler can optimize to a loop (tail call optimization).

C++Tail Recursion
// 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
}
Head Recursion Work after call

Recursive call happens first, work is done on the way back up (during unwinding). Cannot be tail-call optimized.

C++Head Recursion
// 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.
Tree Recursion Multiple calls

Makes more than one recursive call per function. Creates a tree of calls. Classic: Fibonacci.

C++Tree Recursion
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
}
Indirect Recursion A→B→A

Function A calls function B, which calls A again. Rare in interviews but good to know.

C++Indirect Recursion
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
}
βš–οΈ
Recursion vs Iteration
Know when to use which β€” interviewers love this question
AspectRecursionIteration
Code clarityCleaner for tree/graph problemsBetter for simple loops
MemoryO(n) stack framesO(1) extra space
SpeedSlight overhead (function calls)Faster (no call overhead)
Stack overflow riskYes β€” deep recursion crashesNone
ExpressivenessNatural for divide & conquerHarder for tree traversal
DebuggingHarder β€” call stack is deepEasier to trace
Use whenTrees, graphs, backtracking, D&CArrays, simple loops, O(1) space needed
C++ Same Problem β€” Two Approaches
// 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; }
πŸ”·
Recursion Patterns
7 patterns that cover 90% of recursive problems
01 Linear Recursion
One call per step β€Ί

Makes exactly one recursive call per invocation. Simplest form. Used for: traversal, counting, sum, reverse.

C++Linear Recursion Examples
// 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);
}
02 Divide & Conquer
Split β†’ Solve β†’ Merge β€Ί

Split problem into independent halves, solve each recursively, merge results. Examples: Merge Sort, Binary Search, Quick Sort.

C++Merge Sort β€” Classic D&C
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)
03 Pick / Not Pick Pattern
Subsets & Combinations β€Ί

At each index, make a binary choice: include the element OR exclude it. Generates all subsets. Foundation of many backtracking problems.

C++Generate All Subsets
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]
04 Backtracking
Choose→Explore→Undo ›

Try a choice, recurse deeper, then undo the choice (backtrack) to try alternatives. Used when you need all valid solutions.

C++Permutations β€” Classic Backtracking
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
}
05 DFS-style Recursion
Tree & Graph traversal β€Ί

Recursively visit all children/neighbors. Foundation of tree traversals (inorder, preorder, postorder) and graph DFS.

C++Binary Tree Traversals
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));
}
06 Recursion on Index
Array traversal β€Ί

Process array/string by advancing the index recursively. Replaces loops with recursive index movement.

C++Array Sum + String Reverse
// 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));
}
07 Memoized Recursion (Top-Down DP)
O(n) from O(2ⁿ) β€Ί

Cache results of recursive calls. If a subproblem is seen again, return cached value instead of recomputing. Converts exponential to polynomial.

C++Fibonacci with Memoization β€” O(n)
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);
🎯
Must-Solve Problems
Solve in this order β€” basics first, then advanced
#ProblemPatternDifficultyKey Idea
1Print 1 to NLinear / TailEasyBase: n=0, print then recurse or recurse then print
2FactorialLinearEasyn * factorial(n-1)
3FibonacciTreeEasyTwo base cases: fib(0)=0, fib(1)=1
4Power (a^b)D&CEasyb even: power(aΒ²,b/2); odd: aΓ—power(a,b-1)
5Reverse ArrayTwo Pointer RecEasyswap(a[l],a[r]), recurse(l+1,r-1)
6Check PalindromeLinearEasys[l]==s[r] && isPalin(l+1,r-1)
7Subsets / Power SetLeetCode 78Pick/Not PickMedium2 choices per element: include or skip
8PermutationsLeetCode 46BacktrackingMediumswap→recurse→swap back
9Combination SumLeetCode 39BacktrackingMediumCan reuse elements; skip when target<0
10Letter CombinationsLeetCode 17BacktrackingMediumPhone pad mapping, recurse each digit
11Word SearchLeetCode 79DFS + BacktrackMediumMark visited, explore 4 dirs, unmark
12N-QueensLeetCode 51BacktrackingHardPlace queen row by row, check validity
13Sudoku SolverLeetCode 37BacktrackingHardTry 1-9 in each empty cell, backtrack on fail
↩️
Backtracking Deep Dive
The most powerful recursion technique for interviews
🎯 Backtracking = Recursion + Undo. Try a choice β†’ if it leads to a solution, keep it β†’ if not, undo it and try the next choice. Explores all possibilities efficiently by pruning dead-end branches.
Decision Tree for Subsets of [1,2] β€” Pick/Not Pick
start [ ]
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
pick 1 β†’ [1]
skip 1 β†’ [ ]
β”Œβ”€β”€β”€β”€β”˜β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”˜β”€β”€β”€β”€β”
[1,2] βœ“
[1] βœ“
[2] βœ“
[ ] βœ“
Every leaf = one valid subset. 2 elements β†’ 2Β² = 4 subsets. n elements β†’ 2ⁿ subsets.
C++ N-Queens β€” Full Backtracking Template
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;
    }
};
⏱️
Time & Space Complexity
How to analyze recursive complexity β€” the recursion tree method
AlgorithmTimeSpace (Stack)Reason
Factorial(n)O(n)O(n)n linear calls
Fibonacci(n) β€” no memoO(2ⁿ)O(n)2 calls per node, depth n
Fibonacci(n) β€” with memoO(n)O(n)Each subproblem solved once
Merge SortO(n log n)O(n)log n levels Γ— n work per level
Binary SearchO(log n)O(log n)Halves each time
SubsetsO(2ⁿ)O(n)2 choices per element
PermutationsO(n!)O(n)n choices Γ— (n-1) Γ— ... Γ— 1
Power(a,b) fastO(log b)O(log b)Halves exponent each call
πŸ“
Recursion Tree Method:
Time = (number of nodes in recursion tree) Γ— (work per node)
Space = depth of recursion tree (max stack frames at one time)
⚑
Optimization Techniques
From exponential to polynomial β€” the DP gateway
Memoization (Top-Down)

Cache the result of each unique subproblem. On re-encountering it, return cached value. Keeps the recursive structure but eliminates repeated work.

βœ… Best when: recursive structure is natural and not all subproblems are needed.
Tabulation (Bottom-Up)

Convert recursion to iteration. Fill a DP table from base cases upward. No function call overhead, no stack risk.

βœ… Best when: all subproblems must be solved and you need O(1) space (space optimization possible).
C++ Fibonacci: Recursive β†’ Memo β†’ Tabulation β†’ O(1) Space
// 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;
}
⚠️
Common Mistakes
Every beginner makes these β€” don't be one of them
  • 🚫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 n never 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 of return recurse(n-1); β€” the computed value is lost. If your function returns something, use return.
  • 🚫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.
⚑
Quick Revision Cheat Sheet
Last-minute review before your interview
Pattern β†’ Use When
Print/traverseLinear Recursion
All subsetsPick / Not Pick
All permutationsBacktracking
Sort / searchDivide & Conquer
Tree/graphDFS Recursion
Overlapping subproblemsMemoization
Complexity Quick Ref
Factorial / linearO(n) time, O(n) space
Fibonacci (plain)O(2ⁿ)
Fibonacci (memo)O(n)
Merge SortO(n log n)
SubsetsO(2ⁿ)
PermutationsO(n!)
Backtracking Template
void solve(state) {
if (base) { save; return; }
for each choice:
make choice // CHOOSE
solve(next) // EXPLORE
undo choice // UNDO
}
Red Flags in Recursion
No base caseStack overflow
n not decreasingInfinite loop
Missing returnWrong answer
No pop_backStale backtrack state
n > 10⁡Use iteration
Repeated subproblemsAdd memo!
πŸš€
Next: Linked List β†’ Stack & Queue β†’ Trees β†’ Graphs β†’ Dynamic Programming