DSA

Recursion

23 Questions

Recursion is when a function calls itself to solve a smaller instance of the same problem. Every recursive function needs a base case to stop the recursion.

function countdown(n) {
    if (n <= 0) return; // base case
    console.log(n);
    countdown(n - 1);   // recursive call
}
// countdown(3) => 3, 2, 1

Without a base case, recursion leads to infinite calls and a stack overflow.

The base case is the condition that stops recursion. It returns a value directly without making another recursive call, preventing infinite recursion.

function sum(n) {
    if (n === 0) return 0;  // base case: stop here
    return n + sum(n - 1);  // recursive case
}
// sum(4) => 4 + 3 + 2 + 1 + 0 = 10

// Without base case:
// sum(4) -> sum(3) -> sum(2) -> ... forever!

A good base case is simple, reachable, and handles the smallest valid input.

Factorial: n! = n × (n-1)! with base case 0! = 1.

function factorial(n) {
    if (n <= 1) return 1;     // base case
    return n * factorial(n - 1); // recursive step
}
// factorial(5) => 5 * 4 * 3 * 2 * 1 = 120

// Call stack: factorial(5)
//   5 * factorial(4)
//     4 * factorial(3)
//       3 * factorial(2)
//         2 * factorial(1) => 1

Time: O(n)  |  Space: O(n) due to call stack

Naive recursive Fibonacci has exponential time due to repeated calculations. Add memoization to make it O(n).

// Naive — O(2^n)
function fib(n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

// Memoized — O(n)
function fibMemo(n, memo = {}) {
    if (n <= 1) return n;
    if (memo[n]) return memo[n];
    return memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
}
// fibMemo(10) => 55

Memoization caches results of subproblems to avoid redundant work.

Tower of Hanoi: Move n disks from source to target using an auxiliary peg. Move n-1 disks to auxiliary, move the largest to target, then move n-1 from auxiliary to target.

function hanoi(n, from, to, aux) {
    if (n === 1) {
        console.log(from + " -> " + to);
        return;
    }
    hanoi(n - 1, from, aux, to);
    console.log(from + " -> " + to);
    hanoi(n - 1, aux, to, from);
}
// hanoi(3, "A", "C", "B")
// Moves: 2^n - 1 = 7 moves for 3 disks

Moves: 2<sup>n</sup> - 1  |  Time: O(2<sup>n</sup>)

The power set contains all possible subsets. For each element, either include it or exclude it.

function powerSet(arr, idx = 0, current = []) {
    if (idx === arr.length) {
        console.log(current);
        return;
    }
    // Include arr[idx]
    powerSet(arr, idx + 1, [...current, arr[idx]]);
    // Exclude arr[idx]
    powerSet(arr, idx + 1, current);
}
// powerSet([1, 2, 3])
// [], [3], [2], [2,3], [1], [1,3], [1,2], [1,2,3]

Total subsets: 2<sup>n</sup>  |  Time: O(2<sup>n</sup>)

Fix one character at a time and recursively permute the rest. Swap each character into the current position.

function permutations(str, l = 0, r = str.length - 1) {
    if (l === r) { console.log(str); return; }
    const arr = str.split("");
    for (let i = l; i <= r; i++) {
        [arr[l], arr[i]] = [arr[i], arr[l]];
        permutations(arr.join(""), l + 1, r);
        [arr[l], arr[i]] = [arr[i], arr[l]]; // backtrack
    }
}
// permutations("abc") => abc, acb, bac, bca, cba, cab

Total: n! permutations  |  Time: O(n × n!)

Recursive binary search divides the search range by half on each call. The base case is when the range is empty.

function binarySearchRec(arr, target, lo = 0, hi = arr.length - 1) {
    if (lo > hi) return -1; // base case
    const mid = Math.floor((lo + hi) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target)
        return binarySearchRec(arr, target, mid + 1, hi);
    return binarySearchRec(arr, target, lo, mid - 1);
}
// binarySearchRec([1,3,5,7,9], 7) => 3

Time: O(log n)  |  Space: O(log n) due to call stack

Add the first element to the recursive sum of the rest of the array. Base case: empty array returns 0.

function sumArray(arr, i = 0) {
    if (i === arr.length) return 0;  // base case
    return arr[i] + sumArray(arr, i + 1);
}
// sumArray([1, 2, 3, 4, 5]) => 15

// Trace: 1 + sumArray([2,3,4,5])
//        1 + 2 + sumArray([3,4,5])
//        1 + 2 + 3 + sumArray([4,5])
//        1 + 2 + 3 + 4 + sumArray([5])
//        1 + 2 + 3 + 4 + 5 + 0 = 15

Time: O(n)  |  Space: O(n) call stack

Check each element: if it is an array, recursively flatten it; otherwise, add it to the result.

function flatten(arr) {
    const result = [];
    for (const item of arr) {
        if (Array.isArray(item)) {
            result.push(...flatten(item));
        } else {
            result.push(item);
        }
    }
    return result;
}
// flatten([1, [2, [3, 4], 5], [6, 7]])
// => [1, 2, 3, 4, 5, 6, 7]

Time: O(n) where n = total elements  |  Space: O(d) where d = max depth

Move n-1 disks from source to auxiliary (using destination), move the largest disk to destination, then move n-1 disks from auxiliary to destination (using source).

function hanoi(n, from, to, aux) {
    if (n === 0) return;
    hanoi(n - 1, from, aux, to); // move n-1 disks to auxiliary
    console.log("Move disk " + n + " from " + from + " to " + to);
    hanoi(n - 1, aux, to, from); // move n-1 disks from auxiliary to target
}
// Input: n=1 => Output: "Move disk 1 from A to C"  (1 move)
// Input: n=2 => Output: 3 moves (A->B, A->C, B->C)
// Input: n=3 => Output: 7 moves (2^n - 1)
// Input: n=10 => Output: 1023 moves

Time: O(2ⁿ) — minimum moves = 2ⁿ - 1  |  Space: O(n) recursion depth

Place queens row by row. For each row, try each column — only proceed if no existing queen shares the column, or a diagonal.

function solveNQueens(n) {
    const result = [];
    const cols = new Set(), diag1 = new Set(), diag2 = new Set();
    const board = Array.from({length: n}, () => new Array(n).fill('.'));
    function backtrack(row) {
        if (row === n) {
            result.push(board.map(r => r.join('')));
            return;
        }
        for (let col = 0; col < n; col++) {
            if (cols.has(col) || diag1.has(row-col) || diag2.has(row+col)) continue;
            board[row][col] = 'Q';
            cols.add(col); diag1.add(row-col); diag2.add(row+col);
            backtrack(row + 1);
            board[row][col] = '.';
            cols.delete(col); diag1.delete(row-col); diag2.delete(row+col);
        }
    }
    backtrack(0);
    return result;
}
// Input: n=4  => Output: 2 solutions
// Input: n=8  => Output: 92 solutions

Time: O(n!)  |  Space: O(n) for tracking sets

Fix each element at the current position by swapping it with all elements from current to end. Recurse, then unswap (backtrack).

function permutations(nums) {
    const result = [];
    function backtrack(start) {
        if (start === nums.length) { result.push([...nums]); return; }
        for (let i = start; i < nums.length; i++) {
            [nums[start], nums[i]] = [nums[i], nums[start]]; // swap
            backtrack(start + 1);
            [nums[start], nums[i]] = [nums[i], nums[start]]; // unswap
        }
    }
    backtrack(0);
    return result;
}
// Input: [1,2,3]  => Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,2,1],[3,1,2]]
// Input: [1,2]    => Output: [[1,2],[2,1]]
// Input: [1]      => Output: [[1]]

Time: O(n! × n)  |  Space: O(n) recursion depth (not counting output)

GCD uses Euclid's algorithm: gcd(a, b) = gcd(b, a mod b). LCM = (a × b) / gcd(a, b).

function gcd(a, b) {
    return b === 0 ? a : gcd(b, a % b);
}
function lcm(a, b) {
    return (a / gcd(a, b)) * b; // divide first to avoid overflow
}
// Input: gcd(48, 18)   => Output: 6
//   (gcd(48,18) = gcd(18,12) = gcd(12,6) = gcd(6,0) = 6)
// Input: gcd(100, 75)  => Output: 25
// Input: lcm(12, 18)   => Output: 36
// Input: lcm(4, 6)     => Output: 12

Time: O(log(min(a,b))) — Fibonacci worst case  |  Space: O(log(min(a,b)))

Recursively narrow the search space by half. Base case: lo > hi (not found) or arr[mid] equals target.

function binarySearchRecursive(arr, target, lo = 0, hi = arr.length - 1) {
    if (lo > hi) return -1; // base case: not found
    const mid = Math.floor((lo + hi) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) return binarySearchRecursive(arr, target, mid + 1, hi);
    return binarySearchRecursive(arr, target, lo, mid - 1);
}
// Input: [1,3,5,7,9], target=7   => Output: 3 (index)
// Input: [1,3,5,7,9], target=6   => Output: -1 (not found)
// Input: [2,4,6,8,10], target=1  => Output: -1

Time: O(log n)  |  Space: O(log n) recursion stack

For each element, you have two choices: include it or exclude it. Total subsets = 2ⁿ.

function subsets(nums) {
    const result = [];
    function backtrack(start, current) {
        result.push([...current]);
        for (let i = start; i < nums.length; i++) {
            current.push(nums[i]);
            backtrack(i + 1, current);
            current.pop(); // backtrack
        }
    }
    backtrack(0, []);
    return result;
}
// Input: [1,2,3]
// Output: [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]
// (8 = 2³ subsets)
// Input: [1]   => Output: [[], [1]]

Time: O(2ⁿ × n)  |  Space: O(n) recursion depth

Divide the array in half, recursively sort both halves, then merge them. Classic divide-and-conquer with O(n log n) guaranteed.

function mergeSort(arr) {
    if (arr.length <= 1) return arr; // base case
    const mid = Math.floor(arr.length / 2);
    const left  = mergeSort(arr.slice(0, mid));
    const right = mergeSort(arr.slice(mid));
    return merge(left, right);
}
function merge(left, right) {
    const result = [];
    let i = 0, j = 0;
    while (i < left.length && j < right.length) {
        if (left[i] <= right[j]) result.push(left[i++]);
        else result.push(right[j++]);
    }
    return [...result, ...left.slice(i), ...right.slice(j)];
}
// Input: [38,27,43,3,9,82,10]
// Output: [3,9,10,27,38,43,82]

Time: O(n log n)  |  Space: O(n log n) for call stack + temp arrays

Use exponentiation by squaring: x^n = (x^(n/2))² for even n, x × x^(n-1) for odd n. Reduces multiplications from O(n) to O(log n).

function myPow(x, n) {
    if (n < 0) return 1 / myPow(x, -n);
    if (n === 0) return 1;
    if (n % 2 === 0) {
        const half = myPow(x, n / 2);
        return half * half;
    }
    return x * myPow(x, n - 1);
}
// Input: x=2.00, n=10   => Output: 1024.00
// Input: x=2.10, n=3    => Output: 9.261
// Input: x=2.00, n=-2   => Output: 0.25

Time: O(log n)  |  Space: O(log n) stack depth

From each position, try all possible jumps. Memoize: memo[i] = can we reach the end from index i?

function canJump(nums) {
    const memo = new Map();
    function dp(i) {
        if (i >= nums.length - 1) return true;
        if (memo.has(i)) return memo.get(i);
        const maxJump = nums[i];
        for (let j = 1; j <= maxJump; j++) {
            if (dp(i + j)) { memo.set(i, true); return true; }
        }
        memo.set(i, false);
        return false;
    }
    return dp(0);
}
// (Greedy is O(n) but memo recursion shows the principle)
// Input: [2,3,1,1,4]  => Output: true
// Input: [3,2,1,0,4]  => Output: false (stuck at index 3)

Time: O(n²) with memoization  |  Space: O(n)

From (0,0) to (m-1,n-1) moving only right or down. Recursive relation: paths(i,j) = paths(i+1,j) + paths(i,j+1). Memoize to avoid exponential recomputation.

function uniquePaths(m, n) {
    const memo = new Map();
    function dp(r, c) {
        if (r === m - 1 || c === n - 1) return 1; // base: last row/col
        const key = r + ',' + c;
        if (memo.has(key)) return memo.get(key);
        const result = dp(r + 1, c) + dp(r, c + 1);
        memo.set(key, result);
        return result;
    }
    return dp(0, 0);
}
// Input: m=3, n=7   => Output: 28
// Input: m=3, n=2   => Output: 3
// Input: m=1, n=1   => Output: 1

Time: O(m × n)  |  Space: O(m × n)

Use backtracking: recursively build combinations. At each step, choose the next number (must be greater than the last chosen to avoid duplicates).

function combine(n, k) {
    const result = [];
    function backtrack(start, current) {
        if (current.length === k) { result.push([...current]); return; }
        // Pruning: only go up to n - (k - current.length) + 1
        const limit = n - (k - current.length) + 1;
        for (let i = start; i <= limit; i++) {
            current.push(i);
            backtrack(i + 1, current);
            current.pop();
        }
    }
    backtrack(1, []);
    return result;
}
// Input: n=4, k=2   => Output: [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
// Input: n=1, k=1   => Output: [[1]]

Time: O(C(n,k) × k)  |  Space: O(k) recursion depth

Recursively halve the number. If it reaches 1, it's a power of 2. If it becomes odd before reaching 1, it's not. Alternatively, use bitwise: n & (n-1) === 0.

function isPowerOfTwo(n) {
    if (n <= 0) return false;
    if (n === 1) return true; // base case
    if (n % 2 !== 0) return false; // odd means not a power of 2
    return isPowerOfTwo(n / 2);
}
// Bitwise approach (O(1)):
function isPowerOfTwoBit(n) {
    return n > 0 && (n & (n - 1)) === 0;
}
// Input: n=1    => Output: true   (2^0)
// Input: n=16   => Output: true   (2^4)
// Input: n=3    => Output: false
// Input: n=218  => Output: false

Time: O(log n) recursive  |  Space: O(log n). Bitwise is O(1) time and O(1) space.

Each element C(n,k) = C(n-1,k-1) + C(n-1,k). Use memoization to avoid exponential blowup, or use the formula C(n,k) = C(n,k-1) × (n-k+1)/k.

function getRow(rowIndex) {
    const result = [1];
    for (let k = 1; k <= rowIndex; k++) {
        // C(n,k) = C(n,k-1) * (n-k+1) / k
        result.push(Math.round(result[k-1] * (rowIndex - k + 1) / k));
    }
    return result;
}
// Recursive with memo:
function comb(n, k, memo = {}) {
    if (k === 0 || k === n) return 1;
    const key = n + ',' + k;
    if (key in memo) return memo[key];
    return memo[key] = comb(n-1, k-1, memo) + comb(n-1, k, memo);
}
// Input: rowIndex=3   => Output: [1,3,3,1]
// Input: rowIndex=4   => Output: [1,4,6,4,1]
// Input: rowIndex=0   => Output: [1]

Time: O(n) iterative  |  Space: O(n)