← All Posts
DSA Series · Complexity · Part 6

Space Complexity

Time complexity gets all the attention, but space complexity is equally important. Memory is finite, and for large inputs, the algorithm that uses less memory might be the only one that's feasible.

What Counts as Space?

Space complexity measures the maximum amount of memory used at any point during execution, as a function of input size.

Auxiliary space vs. total space

When someone says "merge sort uses $O(n)$ space," they mean auxiliary. The input is always $O(n)$.

What we count

Recursion and Stack Space

Every recursive call pushes a stack frame containing local variables, parameters, and a return address. The maximum number of frames on the stack at once = the maximum recursion depth.

Example 1: Factorial — $O(n)$ space

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);  // each call adds a frame
}

Recursion depth = $n$. Each frame has $O(1)$ variables. Space: $O(n)$.

Example 2: Binary search — $O(\log n)$ space (recursive)

int bsearch(vector<int>& a, int lo, int hi, int t) {
    if (lo > hi) return -1;
    int mid = (lo + hi) / 2;
    if (a[mid] == t) return mid;
    if (a[mid] < t) return bsearch(a, mid+1, hi, t);
    return bsearch(a, lo, mid-1, t);
}

Only one branch recurses (tail-like). Depth = $O(\log n)$. Space: $O(\log n)$.

The iterative version uses $O(1)$ space, no stack frames at all.

Example 3: Post-order tree traversal — $O(h)$ space

void postOrder(TreeNode* node) {
    if (!node) return;
    postOrder(node->left);   // active frames: depth = current level
    postOrder(node->right);
    visit(node);
}

Max recursion depth = tree height $h$. For a balanced tree, $h = O(\log n)$. For a skewed tree, $h = O(n)$.

Common Space Complexity Patterns

AlgorithmTimeAuxiliary SpaceNotes
Linear search$O(n)$$O(1)$Just a loop variable
Binary search (iterative)$O(\log n)$$O(1)$lo, hi, mid
Binary search (recursive)$O(\log n)$$O(\log n)$Stack frames
Merge sort$O(n \log n)$$O(n)$Merge buffer
Quicksort$O(n \log n)$$O(\log n)$Stack; $O(n)$ worst case
Heap sort$O(n \log n)$$O(1)$In-place
BFS$O(V + E)$$O(V)$Queue can hold entire level
DFS (recursive)$O(V + E)$$O(V)$Stack = recursion depth
Dynamic programming (2D)$O(nm)$$O(nm)$Full DP table
DP (space-optimized)$O(nm)$$O(m)$Keep only 2 rows

In-Place Algorithms

An algorithm is in-place if it uses $O(1)$ auxiliary space (ignoring the input). Examples:

// In-place array reversal: O(1) auxiliary space
void reverse(vector<int>& arr) {
    int l = 0, r = arr.size() - 1;
    while (l < r) {
        swap(arr[l], arr[r]);
        l++; r--;
    }
}

The Space-Time Trade-off

Often you can trade space for time or vice versa:

ApproachTimeSpaceExample
No precomputationHigh (recompute)$O(1)$Recompute Fibonacci every time
MemoizationLow (lookup)$O(n)$Cache Fibonacci in array
Hash table$O(1)$ query$O(n)$Two-sum: store complements
Brute force search$O(n)$ query$O(1)$Two-sum: nested loops
Sorting + binary search$O(\log n)$ query$O(1)$ if sortableMiddle ground
Design principle: If the problem allows it, precompute and cache results (use more space) to speed up future queries. If memory is constrained, recompute on the fly.

DP Space Optimization Technique

Many 2D DP problems only need the current and previous row. Instead of maintaining the full table ($O(nm)$), keep just 2 rows ($O(m)$):

// Longest Common Subsequence: O(m) space instead of O(nm)
int lcs(string& a, string& b) {
    int n = a.size(), m = b.size();
    vector<int> prev(m + 1, 0), curr(m + 1, 0);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (a[i-1] == b[j-1])
                curr[j] = prev[j-1] + 1;
            else
                curr[j] = max(prev[j], curr[j-1]);
        }
        swap(prev, curr);
        fill(curr.begin(), curr.end(), 0);
    }
    return prev[m];
}

Time remains $O(nm)$ but space drops from $O(nm)$ to $O(m)$. This technique works whenever $dp[i][j]$ depends only on $dp[i-1][\cdot]$ and $dp[i][j-1]$.

Tail Recursion and Space

A function is tail-recursive if the recursive call is the last thing it does. Some compilers optimize tail calls to reuse the current stack frame, converting $O(n)$ stack space to $O(1)$.

// Not tail-recursive: must multiply after return
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);  // multiplication AFTER recursive call
}

// Tail-recursive version
int factHelper(int n, int acc) {
    if (n <= 1) return acc;
    return factHelper(n - 1, n * acc);  // nothing after recursive call
}

C++ does not guarantee tail-call optimization, but many compilers (GCC, Clang with -O2) do it.

Summary